@odla-ai/chapter 0.48.0 → 0.48.2

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.
@@ -288,7 +288,7 @@ interface LeaderFormationConfig {
288
288
  maxLen?: Record<string, number>;
289
289
  /** Per-string cap when `maxLen` has no field override. Default 4000. */
290
290
  defaultMaxLen?: number;
291
- /** Maximum JSON request size in characters. Default 32768. */
291
+ /** Maximum JSON request size in UTF-8 bytes, enforced while streaming. Default 32768. */
292
292
  bodyCap?: number;
293
293
  }
294
294
  /** Fully resolved formation intake policy. */
@@ -534,7 +534,7 @@ interface ChapterApplication {
534
534
  /** Per-field character cap. Fields not listed use `defaultMaxLen`. */
535
535
  maxLen?: Record<string, number>;
536
536
  defaultMaxLen?: number;
537
- /** Max JSON request body in bytes. Default 32768. */
537
+ /** Max JSON request body in UTF-8 bytes, enforced while streaming. Default 32768. */
538
538
  bodyCap?: number;
539
539
  /** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
540
540
  * writing a row with no consent record. Default `true`. Set `false`
package/dist/index.cjs CHANGED
@@ -270,6 +270,13 @@ module.exports = __toCommonJS(src_exports);
270
270
  // src/config.ts
271
271
  var import_crm2 = require("@odla-ai/crm");
272
272
 
273
+ // src/schema-account-policy.ts
274
+ var chapterAccountPolicy = { attrs: {
275
+ id: { type: "string", unique: true, indexed: true, optional: false },
276
+ metadata: { type: "string", unique: false, indexed: false, optional: false },
277
+ roles: { type: "json", unique: false, indexed: false, optional: false }
278
+ } };
279
+
273
280
  // src/member.ts
274
281
  var import_crm = require("@odla-ai/crm");
275
282
 
@@ -393,8 +400,10 @@ async function submitApplication(db, chapter, fields, opts) {
393
400
  if (fields.focus !== void 0) row2.focus = clampArray(fields.focus, app.maxArrayLen);
394
401
  if (opts.groupId) row2.groupId = opts.groupId;
395
402
  if (opts.runtime) row2.stripeRuntime = opts.runtime;
403
+ if (chapter.account !== "none") row2.clerkAccountPending = true;
396
404
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
397
405
  row2.emailNormalized = normalizeChapterEmail(fields.email);
406
+ row2.signupEmailKey = row2.emailNormalized;
398
407
  }
399
408
  if (opts.admissionGrant?.decision === "waive_interview") {
400
409
  row2.admissionGrantId = opts.admissionGrant.id;
@@ -418,11 +427,21 @@ async function submitApplication(db, chapter, fields, opts) {
418
427
  consumedApplicationId: id10
419
428
  } });
420
429
  }
421
- const { duplicate } = await db.transact(
422
- ops,
423
- opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
424
- );
425
- return { ok: true, id: id10, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
430
+ try {
431
+ const { duplicate } = await db.transact(
432
+ ops,
433
+ opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
434
+ );
435
+ return { ok: true, id: id10, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
436
+ } catch (error) {
437
+ if (error.code === "unique_violation") return {
438
+ ok: false,
439
+ code: "account_already_exists",
440
+ status: 409,
441
+ error: "An account or application already exists for this email. Please sign in to continue."
442
+ };
443
+ throw error;
444
+ }
426
445
  }
427
446
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
428
447
  return {
@@ -497,6 +516,9 @@ var applicationAttrs = {
497
516
  refundPolicyAckAt: attr("number", { optional: true }),
498
517
  prepEmailSentAt: attr("number", { optional: true }),
499
518
  canceled: attr("boolean", { optional: true }),
519
+ signupEmailKey: attr("string", { unique: true, indexed: true, optional: true }),
520
+ clerkInvitationSentAt: attr("number", { optional: true }),
521
+ clerkAccountPending: attr("boolean", { optional: true }),
500
522
  emailNormalized: attr("string", { indexed: true, optional: true }),
501
523
  admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
502
524
  admissionSource: attr("string", { indexed: true, optional: true }),
@@ -1169,6 +1191,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
1169
1191
  entities.memberContentLocal = memberContentLocal;
1170
1192
  }
1171
1193
  }
1194
+ if (auth.source !== "table") entities.chapterAccountPolicy = chapterAccountPolicy;
1172
1195
  if (auth.source === "table") entities.admins = admins;
1173
1196
  if (auth.superAdminSource === "table") entities.superAdmins = superAdmins;
1174
1197
  if (includeNetworkNotes) entities.networkNotes = networkNotes;
@@ -3248,6 +3271,12 @@ function createChapterIntegration(chapter, options = {}) {
3248
3271
  ...emails?.debugEmail ? { debugEmail: emails.debugEmail } : {}
3249
3272
  });
3250
3273
  const seeds = [...crmDesc.seeds ?? []];
3274
+ if (chapter.auth.source !== "table") seeds.push({
3275
+ id: "account-policy",
3276
+ ns: "chapterAccountPolicy",
3277
+ key: { attr: "id", value: chapter.id },
3278
+ attrs: { metadata: chapter.auth.source === "clerk" ? "private" : "public", roles: [...chapter.auth.ladder] }
3279
+ });
3251
3280
  const group = chapter.groupSeed();
3252
3281
  if (group) {
3253
3282
  seeds.push({ id: "group", ns: "groups", key: { attr: "id", value: chapter.id }, attrs: { ...group, createdAt: now } });
@@ -7411,8 +7440,30 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
7411
7440
  );
7412
7441
  }
7413
7442
 
7443
+ // src/clerk-signup.ts
7444
+ async function findSignupAccount(secret, email, fetchImpl = fetch) {
7445
+ const response = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(normalizeChapterEmail(email))}&limit=2`, {
7446
+ headers: { authorization: `Bearer ${secret}` },
7447
+ redirect: "manual",
7448
+ signal: AbortSignal.timeout(5e3)
7449
+ });
7450
+ if (!response.ok) throw new Error("signup account lookup unavailable");
7451
+ const users = await response.json();
7452
+ if (!Array.isArray(users) || users.length > 1 || users.length === 1 && (typeof users[0]?.id !== "string" || !users[0].id)) {
7453
+ throw new Error("signup account lookup unavailable");
7454
+ }
7455
+ return users[0] ?? null;
7456
+ }
7457
+ function belongsToSignup(account, applicationId) {
7458
+ return typeof applicationId === "string" && applicationId.length > 0 && account.private_metadata?.applicationId === applicationId;
7459
+ }
7460
+
7414
7461
  // src/clerk.ts
7415
- var heal = (status2) => status2 === 422 ? { ok: true, status: status2, existed: true } : { ok: false, status: status2 };
7462
+ async function rejectedCreate(res) {
7463
+ const body = typeof res.json === "function" ? await res.json().catch(() => null) : null;
7464
+ const existed = Array.isArray(body?.errors) && body.errors.some((error) => error?.code === "form_identifier_exists" || error?.code === "duplicate_record");
7465
+ return { ok: false, status: res.status, ...existed ? { existed: true } : {} };
7466
+ }
7416
7467
  function clerkInviteRequest(input) {
7417
7468
  return {
7418
7469
  path: "/v1/invitations",
@@ -7431,7 +7482,7 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
7431
7482
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
7432
7483
  body: JSON.stringify(body)
7433
7484
  });
7434
- return res.ok ? { ok: true, status: res.status } : heal(res.status);
7485
+ return res.ok ? { ok: true, status: res.status } : rejectedCreate(res);
7435
7486
  }
7436
7487
  function clerkUserRequest(input) {
7437
7488
  return {
@@ -7483,20 +7534,10 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
7483
7534
  ...typeof body2?.id === "string" && body2.id ? { userId: body2.id } : {}
7484
7535
  };
7485
7536
  }
7486
- const errorBody = res.status === 400 && typeof res.json === "function" ? await res.json().catch(() => null) : null;
7487
- const existingIdentifier = Array.isArray(errorBody?.errors) && errorBody.errors.some((error) => error.code === "form_identifier_exists");
7488
- const healed = existingIdentifier ? { ok: true, status: res.status, existed: true } : heal(res.status);
7489
- const repair = input.repairMetadata ?? {
7490
- ...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
7491
- ...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
7492
- };
7493
- if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
7494
- const userId = await existingClerkUserId(secretKey, input.email, fetchImpl).catch(() => null);
7495
- const refreshed = userId ? await updateClerkUserMetadata(secretKey, userId, {
7496
- ...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
7497
- ...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
7498
- }, fetchImpl).catch(() => false) : false;
7499
- return { ...healed, refreshed, ...refreshed && userId ? { userId } : {} };
7537
+ const failure = await rejectedCreate(res);
7538
+ if (!failure.existed) return failure;
7539
+ const account = await findSignupAccount(secretKey, input.email, fetchImpl).catch(() => null);
7540
+ return account && belongsToSignup(account, input.privateMetadata?.applicationId) ? { ok: true, status: res.status, existed: true, userId: account.id } : failure;
7500
7541
  }
7501
7542
 
7502
7543
  // src/clerk-roles.ts