@odla-ai/chapter 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { CrmConfig, Crm } from '@odla-ai/crm';
2
+ import { initAdmin } from '@odla-ai/db';
2
3
 
3
4
  /** Which feature profile a site runs. `chapter` is the full public member site
4
5
  * (join, Stripe membership, booking, member area, admin, CRM); `hub` is
@@ -188,7 +189,14 @@ interface ChapterConfig {
188
189
  auth?: ChapterAuth;
189
190
  /** odla services (db implied). Default `["db","calendar","o11y"]`. */
190
191
  services?: readonly string[];
192
+ /** Apply-time account provisioning: `"invite"` (default) mints a Clerk
193
+ * invitation, `"create"` makes the account server-side (so join can say the
194
+ * account is ready), `"none"` skips it. Any of these needs a `clerk_secret_key`
195
+ * vault secret to act. */
196
+ account?: AccountModel;
191
197
  }
198
+ /** Apply-time Clerk account provisioning model. */
199
+ type AccountModel = "invite" | "create" | "none";
192
200
  /** The resolved engine `defineChapter()` returns. */
193
201
  interface Chapter {
194
202
  config: ChapterConfig;
@@ -208,6 +216,8 @@ interface Chapter {
208
216
  schema: DbSchema;
209
217
  rules: DbRules;
210
218
  services: readonly string[];
219
+ /** Resolved apply-time account provisioning model (default `"invite"`). */
220
+ account: AccountModel;
211
221
  /** The seed `groups` row derived from config (chapter mode), else `null`. */
212
222
  groupSeed(): Record<string, unknown> | null;
213
223
  }
@@ -246,15 +256,68 @@ interface ChapterWorkerOptions {
246
256
  chapter: Chapter;
247
257
  /** CRM mount point. Default "/api/crm". */
248
258
  crmBasePath?: string;
259
+ /** Host routes, tried BEFORE the built-ins — so a wrapping site can add its own
260
+ * routes (or override/alias a built-in path) and reuse chapter's auth via the
261
+ * shared {@link WorkerContext}, instead of re-verifying JWTs itself. */
262
+ routes?: Route[];
249
263
  }
264
+ /** The registry public-config a site reads to boot Clerk sign-in. */
265
+ type PublicConfig = {
266
+ env?: string;
267
+ clerkPublishableKey?: string | null;
268
+ issuer?: string | null;
269
+ };
270
+ /** The odla-db admin client type. */
271
+ type Db = ReturnType<typeof initAdmin>;
272
+ /** A verified session: the Clerk `sub`, optional email, and the raw JWT payload
273
+ * (so the role claim can be read for auth source "claim"). */
274
+ interface Verified {
275
+ userId: string;
276
+ email?: string;
277
+ payload: Record<string, unknown>;
278
+ }
279
+ /**
280
+ * Build the per-site Worker context: env-independent helpers closing over the
281
+ * public-config and JWKS caches, plus the source-aware auth gate (JWT claim or
282
+ * the odla-db `admins` allowlist). Constructed once per `chapterWorker` and
283
+ * shared by every route module.
284
+ */
285
+ declare function createWorkerContext(options: ChapterWorkerOptions): {
286
+ chapter: Chapter;
287
+ auth: ResolvedAuth;
288
+ crmBase: string;
289
+ getPublicConfig: (env: ChapterEnv) => Promise<PublicConfig>;
290
+ verifyUser: (req: Request, env: ChapterEnv) => Promise<Verified | null>;
291
+ makeDb: (env: ChapterEnv) => Db;
292
+ isAdminEmail: (db: Db, email: string | undefined) => Promise<boolean>;
293
+ isSuperAdminEmail: (db: Db, email: string | undefined) => Promise<boolean>;
294
+ roleFor: (db: Db, u: Verified) => Promise<string>;
295
+ isAdmin: (db: Db, u: Verified) => Promise<boolean>;
296
+ crmSender: (env: ChapterEnv) => {
297
+ send(payload: EmailPayload): Promise<{
298
+ messageId: string;
299
+ }>;
300
+ } | undefined;
301
+ };
302
+ /** The value returned by {@link createWorkerContext}, threaded to every route. */
303
+ type WorkerContext = ReturnType<typeof createWorkerContext>;
304
+ /** A worker route handler: owns the request (returns a Response) or falls through
305
+ * (returns null). Host routes passed to `chapterWorker` compose against the same
306
+ * {@link WorkerContext} the built-ins receive. */
307
+ type Route = (req: Request, url: URL, env: ChapterEnv, ctx: WorkerContext) => Promise<Response | null>;
250
308
 
251
309
  /**
252
310
  * Build the Cloudflare `ExportedHandler` for a chapter/hub site: Clerk-JWT
253
- * verification, the odla-db admins-allowlist gate, the mounted @odla-ai/crm
254
- * routes, the hub→chapter network projection, and the static-asset fallback. In
255
- * hub mode it serves /api/config, /api/me, /api/crm/*, /api/network/shared;
256
- * chapter mode adds the public member surface (join/apply; Stripe + booking
257
- * land in the member-experience release).
311
+ * verification, the source-aware admin gate, the mounted @odla-ai/crm routes, the
312
+ * hub→chapter network projection, and the static-asset fallback. Hub mode serves
313
+ * /api/health, /api/config, /api/me, /api/crm/*, /api/network/shared; chapter mode
314
+ * adds the public member surface (join/apply/pay/book) and the admin surface
315
+ * (/api/admin/*).
316
+ *
317
+ * A wrapping site adds its own routes via `options.routes` — each receives the
318
+ * same {@link WorkerContext} the built-ins get (so it reuses chapter's JWT
319
+ * verify, db client, and role resolution instead of duplicating them), and runs
320
+ * BEFORE the built-ins so it can override or alias a path.
258
321
  *
259
322
  * Observability is a host concern, not a chapter dependency. To trace, wrap the
260
323
  * result in your worker entry — `export default withObservability(chapterWorker(
@@ -265,4 +328,4 @@ declare function chapterWorker(options: ChapterWorkerOptions): {
265
328
  fetch(req: Request, env: ChapterEnv): Promise<Response>;
266
329
  };
267
330
 
268
- export { type ChapterEnv, type ChapterWorkerOptions, chapterWorker };
331
+ export { type ChapterEnv, type ChapterWorkerOptions, type Route, type WorkerContext, chapterWorker, createWorkerContext };
@@ -1,4 +1,5 @@
1
1
  import { CrmConfig, Crm } from '@odla-ai/crm';
2
+ import { initAdmin } from '@odla-ai/db';
2
3
 
3
4
  /** Which feature profile a site runs. `chapter` is the full public member site
4
5
  * (join, Stripe membership, booking, member area, admin, CRM); `hub` is
@@ -188,7 +189,14 @@ interface ChapterConfig {
188
189
  auth?: ChapterAuth;
189
190
  /** odla services (db implied). Default `["db","calendar","o11y"]`. */
190
191
  services?: readonly string[];
192
+ /** Apply-time account provisioning: `"invite"` (default) mints a Clerk
193
+ * invitation, `"create"` makes the account server-side (so join can say the
194
+ * account is ready), `"none"` skips it. Any of these needs a `clerk_secret_key`
195
+ * vault secret to act. */
196
+ account?: AccountModel;
191
197
  }
198
+ /** Apply-time Clerk account provisioning model. */
199
+ type AccountModel = "invite" | "create" | "none";
192
200
  /** The resolved engine `defineChapter()` returns. */
193
201
  interface Chapter {
194
202
  config: ChapterConfig;
@@ -208,6 +216,8 @@ interface Chapter {
208
216
  schema: DbSchema;
209
217
  rules: DbRules;
210
218
  services: readonly string[];
219
+ /** Resolved apply-time account provisioning model (default `"invite"`). */
220
+ account: AccountModel;
211
221
  /** The seed `groups` row derived from config (chapter mode), else `null`. */
212
222
  groupSeed(): Record<string, unknown> | null;
213
223
  }
@@ -246,15 +256,68 @@ interface ChapterWorkerOptions {
246
256
  chapter: Chapter;
247
257
  /** CRM mount point. Default "/api/crm". */
248
258
  crmBasePath?: string;
259
+ /** Host routes, tried BEFORE the built-ins — so a wrapping site can add its own
260
+ * routes (or override/alias a built-in path) and reuse chapter's auth via the
261
+ * shared {@link WorkerContext}, instead of re-verifying JWTs itself. */
262
+ routes?: Route[];
249
263
  }
264
+ /** The registry public-config a site reads to boot Clerk sign-in. */
265
+ type PublicConfig = {
266
+ env?: string;
267
+ clerkPublishableKey?: string | null;
268
+ issuer?: string | null;
269
+ };
270
+ /** The odla-db admin client type. */
271
+ type Db = ReturnType<typeof initAdmin>;
272
+ /** A verified session: the Clerk `sub`, optional email, and the raw JWT payload
273
+ * (so the role claim can be read for auth source "claim"). */
274
+ interface Verified {
275
+ userId: string;
276
+ email?: string;
277
+ payload: Record<string, unknown>;
278
+ }
279
+ /**
280
+ * Build the per-site Worker context: env-independent helpers closing over the
281
+ * public-config and JWKS caches, plus the source-aware auth gate (JWT claim or
282
+ * the odla-db `admins` allowlist). Constructed once per `chapterWorker` and
283
+ * shared by every route module.
284
+ */
285
+ declare function createWorkerContext(options: ChapterWorkerOptions): {
286
+ chapter: Chapter;
287
+ auth: ResolvedAuth;
288
+ crmBase: string;
289
+ getPublicConfig: (env: ChapterEnv) => Promise<PublicConfig>;
290
+ verifyUser: (req: Request, env: ChapterEnv) => Promise<Verified | null>;
291
+ makeDb: (env: ChapterEnv) => Db;
292
+ isAdminEmail: (db: Db, email: string | undefined) => Promise<boolean>;
293
+ isSuperAdminEmail: (db: Db, email: string | undefined) => Promise<boolean>;
294
+ roleFor: (db: Db, u: Verified) => Promise<string>;
295
+ isAdmin: (db: Db, u: Verified) => Promise<boolean>;
296
+ crmSender: (env: ChapterEnv) => {
297
+ send(payload: EmailPayload): Promise<{
298
+ messageId: string;
299
+ }>;
300
+ } | undefined;
301
+ };
302
+ /** The value returned by {@link createWorkerContext}, threaded to every route. */
303
+ type WorkerContext = ReturnType<typeof createWorkerContext>;
304
+ /** A worker route handler: owns the request (returns a Response) or falls through
305
+ * (returns null). Host routes passed to `chapterWorker` compose against the same
306
+ * {@link WorkerContext} the built-ins receive. */
307
+ type Route = (req: Request, url: URL, env: ChapterEnv, ctx: WorkerContext) => Promise<Response | null>;
250
308
 
251
309
  /**
252
310
  * Build the Cloudflare `ExportedHandler` for a chapter/hub site: Clerk-JWT
253
- * verification, the odla-db admins-allowlist gate, the mounted @odla-ai/crm
254
- * routes, the hub→chapter network projection, and the static-asset fallback. In
255
- * hub mode it serves /api/config, /api/me, /api/crm/*, /api/network/shared;
256
- * chapter mode adds the public member surface (join/apply; Stripe + booking
257
- * land in the member-experience release).
311
+ * verification, the source-aware admin gate, the mounted @odla-ai/crm routes, the
312
+ * hub→chapter network projection, and the static-asset fallback. Hub mode serves
313
+ * /api/health, /api/config, /api/me, /api/crm/*, /api/network/shared; chapter mode
314
+ * adds the public member surface (join/apply/pay/book) and the admin surface
315
+ * (/api/admin/*).
316
+ *
317
+ * A wrapping site adds its own routes via `options.routes` — each receives the
318
+ * same {@link WorkerContext} the built-ins get (so it reuses chapter's JWT
319
+ * verify, db client, and role resolution instead of duplicating them), and runs
320
+ * BEFORE the built-ins so it can override or alias a path.
258
321
  *
259
322
  * Observability is a host concern, not a chapter dependency. To trace, wrap the
260
323
  * result in your worker entry — `export default withObservability(chapterWorker(
@@ -265,4 +328,4 @@ declare function chapterWorker(options: ChapterWorkerOptions): {
265
328
  fetch(req: Request, env: ChapterEnv): Promise<Response>;
266
329
  };
267
330
 
268
- export { type ChapterEnv, type ChapterWorkerOptions, chapterWorker };
331
+ export { type ChapterEnv, type ChapterWorkerOptions, type Route, type WorkerContext, chapterWorker, createWorkerContext };