@odla-ai/chapter 0.7.0 → 0.9.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.
@@ -189,7 +189,14 @@ interface ChapterConfig {
189
189
  auth?: ChapterAuth;
190
190
  /** odla services (db implied). Default `["db","calendar","o11y"]`. */
191
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;
192
197
  }
198
+ /** Apply-time Clerk account provisioning model. */
199
+ type AccountModel = "invite" | "create" | "none";
193
200
  /** The resolved engine `defineChapter()` returns. */
194
201
  interface Chapter {
195
202
  config: ChapterConfig;
@@ -209,6 +216,8 @@ interface Chapter {
209
216
  schema: DbSchema;
210
217
  rules: DbRules;
211
218
  services: readonly string[];
219
+ /** Resolved apply-time account provisioning model (default `"invite"`). */
220
+ account: AccountModel;
212
221
  /** The seed `groups` row derived from config (chapter mode), else `null`. */
213
222
  groupSeed(): Record<string, unknown> | null;
214
223
  }
@@ -189,7 +189,14 @@ interface ChapterConfig {
189
189
  auth?: ChapterAuth;
190
190
  /** odla services (db implied). Default `["db","calendar","o11y"]`. */
191
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;
192
197
  }
198
+ /** Apply-time Clerk account provisioning model. */
199
+ type AccountModel = "invite" | "create" | "none";
193
200
  /** The resolved engine `defineChapter()` returns. */
194
201
  interface Chapter {
195
202
  config: ChapterConfig;
@@ -209,6 +216,8 @@ interface Chapter {
209
216
  schema: DbSchema;
210
217
  rules: DbRules;
211
218
  services: readonly string[];
219
+ /** Resolved apply-time account provisioning model (default `"invite"`). */
220
+ account: AccountModel;
212
221
  /** The seed `groups` row derived from config (chapter mode), else `null`. */
213
222
  groupSeed(): Record<string, unknown> | null;
214
223
  }
@@ -95,13 +95,15 @@ function createWorkerContext(options) {
95
95
  import { createCrmRoutes } from "@odla-ai/crm";
96
96
 
97
97
  // src/clerk.ts
98
+ var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
98
99
  function clerkInviteRequest(input) {
99
100
  return {
100
101
  path: "/v1/invitations",
101
102
  body: {
102
103
  email_address: input.email,
103
104
  notify: true,
104
- ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {}
105
+ ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
106
+ ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
105
107
  }
106
108
  };
107
109
  }
@@ -112,7 +114,28 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
112
114
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
113
115
  body: JSON.stringify(body)
114
116
  });
115
- return { ok: res.ok, status: res.status };
117
+ return res.ok ? { ok: true, status: res.status } : heal(res.status);
118
+ }
119
+ function clerkUserRequest(input) {
120
+ return {
121
+ path: "/v1/users",
122
+ body: {
123
+ email_address: [input.email],
124
+ skip_password_requirement: true,
125
+ ...input.firstName ? { first_name: input.firstName } : {},
126
+ ...input.lastName ? { last_name: input.lastName } : {},
127
+ ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
128
+ }
129
+ };
130
+ }
131
+ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
132
+ const { path, body } = clerkUserRequest(input);
133
+ const res = await fetchImpl(`https://api.clerk.com${path}`, {
134
+ method: "POST",
135
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
136
+ body: JSON.stringify(body)
137
+ });
138
+ return res.ok ? { ok: true, status: res.status } : heal(res.status);
116
139
  }
117
140
 
118
141
  // src/member.ts
@@ -428,10 +451,15 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
428
451
  );
429
452
  } catch {
430
453
  }
431
- try {
432
- const secret = await getVaultSecret(db, "clerk_secret_key");
433
- if (secret) await createClerkInvitation(secret, { email });
434
- } catch {
454
+ if (chapter.account !== "none") {
455
+ try {
456
+ const secret = await getVaultSecret(db, "clerk_secret_key");
457
+ if (secret) {
458
+ if (chapter.account === "create") await createClerkUser(secret, { email, firstName: s(fields.firstName), lastName: s(fields.lastName) });
459
+ else await createClerkInvitation(secret, { email });
460
+ }
461
+ } catch {
462
+ }
435
463
  }
436
464
  }
437
465
  async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {