mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-26 13:34:38 +02:00
feat(auth): bootstrap per-Space kontextDoc on Space-creation (F4 follow-up)
Symmetrically extends the F4 server-side singleton bootstrap to the per-Space `kontextDoc`. Every Space-creation — Personal at signup and brand/club/family/team/practice via the org plugin — now writes an empty kontextDoc row straight into mana_sync.sync_changes with origin='system', client_id='system:bootstrap'. Fresh clients pull the row instead of racing on a local insert that the next pull would clobber. - New `bootstrapSpaceSingletons(spaceId, ownerUserId, syncSql)` in services/mana-auth/src/services/bootstrap-singletons.ts; shared `buildFieldMeta` helper extracted. - `createBetterAuth(databaseUrl, syncDatabaseUrl, webauthn)` now takes the sync-DB URL and lazy-creates a module-scoped postgres pool for the bootstrap inserts. - Hook into `databaseHooks.user.create.after` (only on `created: true` from createPersonalSpaceFor) and `organizationHooks.afterCreateOrganization`. - Webapp `kontextStore.ensureDoc()` made private as `getOrCreateLocalDoc()` — same fallback role as userContextStore's after F5. Public API is now just setContent + appendContent. Plan: docs/plans/sync-field-meta-overhaul.md (F4-fu row in Shipping Log). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bcf150ea16
commit
3df7391905
8 changed files with 197 additions and 66 deletions
|
|
@ -21,6 +21,8 @@ import { organization } from 'better-auth/plugins/organization';
|
|||
import { twoFactor } from 'better-auth/plugins/two-factor';
|
||||
import { magicLink } from 'better-auth/plugins/magic-link';
|
||||
import { passkey } from '@better-auth/passkey';
|
||||
import postgres from 'postgres';
|
||||
import { logger } from '@mana/shared-hono';
|
||||
import { getDb } from '../db/connection';
|
||||
import { organizations, members, invitations } from '../db/schema/organizations';
|
||||
import {
|
||||
|
|
@ -45,6 +47,7 @@ import {
|
|||
assertSpaceIsDeletable,
|
||||
createPersonalSpaceFor,
|
||||
} from '../spaces';
|
||||
import { bootstrapSpaceSingletons } from '../services/bootstrap-singletons';
|
||||
|
||||
// Re-export so existing imports (`import { TRUSTED_ORIGINS } from './better-auth.config'`)
|
||||
// keep working. New code should import from './sso-origins' directly.
|
||||
|
|
@ -94,13 +97,30 @@ export interface BetterAuthWebAuthnOptions {
|
|||
/**
|
||||
* Create Better Auth instance
|
||||
*
|
||||
* @param databaseUrl - PostgreSQL connection URL
|
||||
* @param databaseUrl - PostgreSQL connection URL for the auth DB
|
||||
* @param syncDatabaseUrl - PostgreSQL connection URL for `mana_sync`. The
|
||||
* personal-space + organization hooks bootstrap per-Space singletons
|
||||
* into `sync_changes` so fresh clients pull the row instead of racing
|
||||
* on a local insert. See `bootstrapSpaceSingletons`.
|
||||
* @param webauthn - WebAuthn settings for the passkey plugin
|
||||
* @returns Better Auth instance
|
||||
*/
|
||||
export function createBetterAuth(databaseUrl: string, webauthn: BetterAuthWebAuthnOptions) {
|
||||
export function createBetterAuth(
|
||||
databaseUrl: string,
|
||||
syncDatabaseUrl: string,
|
||||
webauthn: BetterAuthWebAuthnOptions
|
||||
) {
|
||||
const db = getDb(databaseUrl);
|
||||
|
||||
// Lazy module-scoped sync SQL pool. Mirrors the pattern in
|
||||
// routes/auth.ts so we don't open a second pool just for the
|
||||
// org-create hook. Process lifetime owns it; never closed manually.
|
||||
let _syncSql: ReturnType<typeof postgres> | null = null;
|
||||
const getSyncSql = (): ReturnType<typeof postgres> => {
|
||||
if (!_syncSql) _syncSql = postgres(syncDatabaseUrl, { max: 2 });
|
||||
return _syncSql;
|
||||
};
|
||||
|
||||
return betterAuth({
|
||||
// Database adapter (Drizzle with PostgreSQL)
|
||||
database: drizzleAdapter(db, {
|
||||
|
|
@ -246,12 +266,30 @@ export function createBetterAuth(databaseUrl: string, webauthn: BetterAuthWebAut
|
|||
user: {
|
||||
create: {
|
||||
after: async (user) => {
|
||||
await createPersonalSpaceFor(db, {
|
||||
const result = await createPersonalSpaceFor(db, {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
accessTier: (user as { accessTier?: string | null }).accessTier,
|
||||
});
|
||||
// Bootstrap the personal Space's kontextDoc only on a
|
||||
// real first-time creation. The `created: false` path
|
||||
// means a previous signup retry already provisioned it
|
||||
// and the doc has been bootstrapped before. Failures
|
||||
// are logged but do not abort signup — the webapp's
|
||||
// `ensureDoc()` fallback still creates the row on the
|
||||
// first write attempt.
|
||||
if (result.created) {
|
||||
bootstrapSpaceSingletons(result.organizationId, user.id, getSyncSql()).catch(
|
||||
(err: unknown) => {
|
||||
logger.error('[auth] bootstrapSpaceSingletons (personal) failed', {
|
||||
userId: user.id,
|
||||
organizationId: result.organizationId,
|
||||
err: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -340,13 +378,32 @@ export function createBetterAuth(databaseUrl: string, webauthn: BetterAuthWebAut
|
|||
/**
|
||||
* Spaces — enforce that every organization carries a valid
|
||||
* `metadata.type` (the Space type), and block deletion of the
|
||||
* user's personal space. See docs/plans/spaces-foundation.md
|
||||
* user's personal space. After-create bootstraps per-Space
|
||||
* singletons (currently `kontextDoc`) into mana_sync so fresh
|
||||
* clients pull the row instead of racing on a local insert.
|
||||
* Personal-space gets the same bootstrap, but from
|
||||
* `databaseHooks.user.create.after` because Better Auth's
|
||||
* `afterCreateOrganization` does not fire on the implicit
|
||||
* personal-space creation that runs inside the user-create
|
||||
* hook (createPersonalSpaceFor writes to `organizations`
|
||||
* directly via Drizzle). See docs/plans/spaces-foundation.md
|
||||
* and ../spaces/metadata.ts.
|
||||
*/
|
||||
organizationHooks: {
|
||||
beforeCreateOrganization: async ({ organization }) => {
|
||||
assertValidSpaceMetadataForCreate(organization.metadata);
|
||||
},
|
||||
afterCreateOrganization: async ({ organization, user }) => {
|
||||
bootstrapSpaceSingletons(organization.id, user.id, getSyncSql()).catch(
|
||||
(err: unknown) => {
|
||||
logger.error('[auth] bootstrapSpaceSingletons (org-hook) failed', {
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
err: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
beforeDeleteOrganization: async ({ organization }) => {
|
||||
assertSpaceIsDeletable(organization.metadata);
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue