@odla-ai/chapter 0.48.0 → 0.48.1

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.
@@ -340,54 +340,6 @@ function createWorkerContext(options) {
340
340
  };
341
341
  }
342
342
 
343
- // src/worker-routes.ts
344
- import { createCrmRoutes } from "@odla-ai/crm";
345
-
346
- // src/discussion-reference-security.ts
347
- async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
348
- const match = req.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/);
349
- const appIncarnation = env.ODLA_APP_INCARNATION;
350
- const expectedTenant = env.ODLA_ENV === "prod" ? env.ODLA_APP_ID : `${env.ODLA_APP_ID}--${env.ODLA_ENV}`;
351
- if (!match || env.ODLA_TENANT !== expectedTenant || typeof appIncarnation !== "string" || !/^[a-f0-9]{32}$/.test(appIncarnation)) return null;
352
- const requestUrl = new URL(req.url);
353
- requestUrl.search = "";
354
- requestUrl.hash = "";
355
- const audience = requestUrl.href;
356
- try {
357
- const url = new URL(
358
- "/registry/discuss/reference-authority/verify",
359
- env.ODLA_PLATFORM
360
- );
361
- const response = await fetcher(url, {
362
- method: "POST",
363
- headers: { "content-type": "application/json" },
364
- body: JSON.stringify({
365
- token: match[1],
366
- appId: env.ODLA_APP_ID,
367
- appIncarnation,
368
- env: env.ODLA_ENV,
369
- product: "crm",
370
- projectCapability: "crm.read",
371
- audience
372
- }),
373
- redirect: "manual"
374
- });
375
- if (!response.ok) return null;
376
- const value = await response.json();
377
- if (value.authority?.appId !== env.ODLA_APP_ID || value.authority.appIncarnation !== appIncarnation || value.authority.appEnv !== env.ODLA_ENV || value.authority.capability !== "product.reference.read" || value.authority.product !== "crm" || value.authority.projectCapability !== "crm.read" || value.authority.audience !== audience || typeof value.actor?.id !== "string" || value.actor.kind !== "human" && value.actor.kind !== "agent" || value.actor.email !== void 0 && typeof value.actor.email !== "string") return null;
378
- return {
379
- userId: value.actor.id,
380
- discussionReferenceScope: "all",
381
- ...value.actor.email ? { email: value.actor.email } : {}
382
- };
383
- } catch {
384
- return null;
385
- }
386
- }
387
-
388
- // src/member.ts
389
- import { assertFieldCondition, resolveFieldStates } from "@odla-ai/crm";
390
-
391
343
  // src/application-id.ts
392
344
  var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
393
345
  async function applicationIdForSubmission(submissionId) {
@@ -399,7 +351,26 @@ async function applicationIdForSubmission(submissionId) {
399
351
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
400
352
  }
401
353
 
354
+ // src/clerk-signup.ts
355
+ async function findSignupAccount(secret, email, fetchImpl = fetch) {
356
+ const response = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(normalizeChapterEmail(email))}&limit=2`, {
357
+ headers: { authorization: `Bearer ${secret}` },
358
+ redirect: "manual",
359
+ signal: AbortSignal.timeout(5e3)
360
+ });
361
+ if (!response.ok) throw new Error("signup account lookup unavailable");
362
+ const users = await response.json();
363
+ if (!Array.isArray(users) || users.length > 1 || users.length === 1 && (typeof users[0]?.id !== "string" || !users[0].id)) {
364
+ throw new Error("signup account lookup unavailable");
365
+ }
366
+ return users[0] ?? null;
367
+ }
368
+ function belongsToSignup(account, applicationId) {
369
+ return typeof applicationId === "string" && applicationId.length > 0 && account.private_metadata?.applicationId === applicationId;
370
+ }
371
+
402
372
  // src/member.ts
373
+ import { assertFieldCondition, resolveFieldStates } from "@odla-ai/crm";
403
374
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
404
375
  var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
405
376
  var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
@@ -503,8 +474,10 @@ async function submitApplication(db, chapter, fields, opts) {
503
474
  if (fields.focus !== void 0) row2.focus = clampArray(fields.focus, app.maxArrayLen);
504
475
  if (opts.groupId) row2.groupId = opts.groupId;
505
476
  if (opts.runtime) row2.stripeRuntime = opts.runtime;
477
+ if (chapter.account !== "none") row2.clerkAccountPending = true;
506
478
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
507
479
  row2.emailNormalized = normalizeChapterEmail(fields.email);
480
+ row2.signupEmailKey = row2.emailNormalized;
508
481
  }
509
482
  if (opts.admissionGrant?.decision === "waive_interview") {
510
483
  row2.admissionGrantId = opts.admissionGrant.id;
@@ -528,11 +501,21 @@ async function submitApplication(db, chapter, fields, opts) {
528
501
  consumedApplicationId: id6
529
502
  } });
530
503
  }
531
- const { duplicate } = await db.transact(
532
- ops,
533
- opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
534
- );
535
- return { ok: true, id: id6, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
504
+ try {
505
+ const { duplicate } = await db.transact(
506
+ ops,
507
+ opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
508
+ );
509
+ return { ok: true, id: id6, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
510
+ } catch (error) {
511
+ if (error.code === "unique_violation") return {
512
+ ok: false,
513
+ code: "account_already_exists",
514
+ status: 409,
515
+ error: "An account or application already exists for this email. Please sign in to continue."
516
+ };
517
+ throw error;
518
+ }
536
519
  }
537
520
  function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
538
521
  return {
@@ -555,6 +538,82 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
555
538
  };
556
539
  }
557
540
 
541
+ // src/signup-eligibility.ts
542
+ var signupConflict = { code: "account_already_exists", message: "An account or application already exists for this email. Please sign in to continue." };
543
+ async function checkSignupEligibility(db, chapter, fields, submissionId) {
544
+ if (typeof fields.email !== "string" || !isValidEmail(fields.email.trim())) return null;
545
+ const email = normalizeChapterEmail(fields.email);
546
+ const applicationId = submissionId ? await applicationIdForSubmission(submissionId) : void 0;
547
+ const conflict = () => Response.json({ error: signupConflict }, { status: 409 });
548
+ try {
549
+ const data = await db.query({
550
+ applications: { $: { where: { emailNormalized: email }, limit: 2 } },
551
+ $users: { $: { where: { email }, limit: 2 } }
552
+ });
553
+ if ((data.applications ?? []).some((row2) => row2.id !== applicationId)) return conflict();
554
+ const original = (data.applications ?? []).find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
555
+ if ((data.$users ?? []).some((row2) => row2.deleted !== true && row2.id !== original?.clerkUserId)) return conflict();
556
+ if (chapter.account === "none") return null;
557
+ const secret = await getVaultSecret(db, "clerk_secret_key");
558
+ if (!secret) return Response.json({ error: { code: "clerk_secret_unavailable", message: "account provisioning credential is unavailable" } }, { status: 503 });
559
+ const account = await findSignupAccount(secret, email);
560
+ if (account && !(original && (original.clerkUserId === account.id || belongsToSignup(account, original.id)))) return conflict();
561
+ return null;
562
+ } catch {
563
+ return Response.json({ error: { code: "signup_lookup_unavailable", message: "Signup is temporarily unavailable. Please try again." } }, { status: 503 });
564
+ }
565
+ }
566
+ async function rejectUnprovisionedSignup(db, applicationId, status2) {
567
+ await db.transact([{ t: "update", ns: "applications", id: applicationId, attrs: { status: "declined" } }], {
568
+ guards: [{ ns: "applications", id: applicationId, exists: true, equals: { status: status2 }, absent: ["clerkUserId"] }]
569
+ });
570
+ }
571
+
572
+ // src/worker-routes.ts
573
+ import { createCrmRoutes } from "@odla-ai/crm";
574
+
575
+ // src/discussion-reference-security.ts
576
+ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
577
+ const match = req.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/);
578
+ const appIncarnation = env.ODLA_APP_INCARNATION;
579
+ const expectedTenant = env.ODLA_ENV === "prod" ? env.ODLA_APP_ID : `${env.ODLA_APP_ID}--${env.ODLA_ENV}`;
580
+ if (!match || env.ODLA_TENANT !== expectedTenant || typeof appIncarnation !== "string" || !/^[a-f0-9]{32}$/.test(appIncarnation)) return null;
581
+ const requestUrl = new URL(req.url);
582
+ requestUrl.search = "";
583
+ requestUrl.hash = "";
584
+ const audience = requestUrl.href;
585
+ try {
586
+ const url = new URL(
587
+ "/registry/discuss/reference-authority/verify",
588
+ env.ODLA_PLATFORM
589
+ );
590
+ const response = await fetcher(url, {
591
+ method: "POST",
592
+ headers: { "content-type": "application/json" },
593
+ body: JSON.stringify({
594
+ token: match[1],
595
+ appId: env.ODLA_APP_ID,
596
+ appIncarnation,
597
+ env: env.ODLA_ENV,
598
+ product: "crm",
599
+ projectCapability: "crm.read",
600
+ audience
601
+ }),
602
+ redirect: "manual"
603
+ });
604
+ if (!response.ok) return null;
605
+ const value = await response.json();
606
+ if (value.authority?.appId !== env.ODLA_APP_ID || value.authority.appIncarnation !== appIncarnation || value.authority.appEnv !== env.ODLA_ENV || value.authority.capability !== "product.reference.read" || value.authority.product !== "crm" || value.authority.projectCapability !== "crm.read" || value.authority.audience !== audience || typeof value.actor?.id !== "string" || value.actor.kind !== "human" && value.actor.kind !== "agent" || value.actor.email !== void 0 && typeof value.actor.email !== "string") return null;
607
+ return {
608
+ userId: value.actor.id,
609
+ discussionReferenceScope: "all",
610
+ ...value.actor.email ? { email: value.actor.email } : {}
611
+ };
612
+ } catch {
613
+ return null;
614
+ }
615
+ }
616
+
558
617
  // src/tiers.ts
559
618
  function tierIsFree(tier) {
560
619
  return !(tier.priceCents > 0) && !tier.stripePriceId;
@@ -3539,13 +3598,13 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
3539
3598
  const normalizedRows = await lookup({ emailNormalized: normalized });
3540
3599
  const legacyRows = await lookup({ email: identity.email });
3541
3600
  apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
3542
- if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0) return null;
3601
+ if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
3543
3602
  emailFallback = true;
3544
3603
  }
3545
3604
  let app = apps.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
3546
3605
  if (!app) return null;
3547
3606
  if (emailFallback) {
3548
- const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status"];
3607
+ const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
3549
3608
  try {
3550
3609
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity.userId } }], {
3551
3610
  mutationId: `member-account-link:${app.id}:${identity.userId}`,
@@ -3579,7 +3638,11 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
3579
3638
  }
3580
3639
 
3581
3640
  // src/clerk.ts
3582
- var heal = (status2) => status2 === 422 ? { ok: true, status: status2, existed: true } : { ok: false, status: status2 };
3641
+ async function rejectedCreate(res) {
3642
+ const body = typeof res.json === "function" ? await res.json().catch(() => null) : null;
3643
+ const existed = Array.isArray(body?.errors) && body.errors.some((error) => error?.code === "form_identifier_exists" || error?.code === "duplicate_record");
3644
+ return { ok: false, status: res.status, ...existed ? { existed: true } : {} };
3645
+ }
3583
3646
  function clerkInviteRequest(input) {
3584
3647
  return {
3585
3648
  path: "/v1/invitations",
@@ -3598,7 +3661,7 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
3598
3661
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
3599
3662
  body: JSON.stringify(body)
3600
3663
  });
3601
- return res.ok ? { ok: true, status: res.status } : heal(res.status);
3664
+ return res.ok ? { ok: true, status: res.status } : rejectedCreate(res);
3602
3665
  }
3603
3666
  function clerkUserRequest(input) {
3604
3667
  return {
@@ -3624,17 +3687,6 @@ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fet
3624
3687
  });
3625
3688
  return res.ok;
3626
3689
  }
3627
- async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
3628
- const id6 = await existingClerkUserId(secretKey, email, fetchImpl);
3629
- return id6 ? updateClerkUserMetadata(secretKey, id6, input, fetchImpl) : false;
3630
- }
3631
- async function existingClerkUserId(secretKey, email, fetchImpl) {
3632
- const auth = { authorization: `Bearer ${secretKey}` };
3633
- const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=2`, { headers: auth });
3634
- if (!found.ok) return null;
3635
- const users = await found.json().catch(() => null);
3636
- return Array.isArray(users) && users.length === 1 && typeof users[0]?.id === "string" ? users[0].id : null;
3637
- }
3638
3690
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
3639
3691
  const { path, body } = clerkUserRequest(input);
3640
3692
  const res = await fetchImpl(`https://api.clerk.com${path}`, {
@@ -3650,20 +3702,10 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
3650
3702
  ...typeof body2?.id === "string" && body2.id ? { userId: body2.id } : {}
3651
3703
  };
3652
3704
  }
3653
- const errorBody = res.status === 400 && typeof res.json === "function" ? await res.json().catch(() => null) : null;
3654
- const existingIdentifier = Array.isArray(errorBody?.errors) && errorBody.errors.some((error) => error.code === "form_identifier_exists");
3655
- const healed = existingIdentifier ? { ok: true, status: res.status, existed: true } : heal(res.status);
3656
- const repair = input.repairMetadata ?? {
3657
- ...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
3658
- ...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
3659
- };
3660
- if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
3661
- const userId = await existingClerkUserId(secretKey, input.email, fetchImpl).catch(() => null);
3662
- const refreshed = userId ? await updateClerkUserMetadata(secretKey, userId, {
3663
- ...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
3664
- ...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
3665
- }, fetchImpl).catch(() => false) : false;
3666
- return { ...healed, refreshed, ...refreshed && userId ? { userId } : {} };
3705
+ const failure = await rejectedCreate(res);
3706
+ if (!failure.existed) return failure;
3707
+ const account = await findSignupAccount(secretKey, input.email, fetchImpl).catch(() => null);
3708
+ return account && belongsToSignup(account, input.privateMetadata?.applicationId) ? { ok: true, status: res.status, existed: true, userId: account.id } : failure;
3667
3709
  }
3668
3710
 
3669
3711
  // src/crm-provisioned-identity.ts
@@ -3718,6 +3760,7 @@ async function markPrivateMetadataSynced(db, applicationId, clerkUserId) {
3718
3760
  id: applicationId,
3719
3761
  attrs: {
3720
3762
  clerkPrivateMetadataSyncedAt: Date.now(),
3763
+ clerkAccountPending: false,
3721
3764
  ...clerkUserId ? { clerkUserId } : {}
3722
3765
  }
3723
3766
  }]);
@@ -3772,7 +3815,7 @@ async function provisionApplicantForRoute(db, chapter, applicationId, fields) {
3772
3815
  body: {
3773
3816
  error: {
3774
3817
  code: error.code,
3775
- message: error.code === "clerk_secret_unavailable" ? "account provisioning credential is unavailable" : "account provisioning failed"
3818
+ message: error.code === "account_already_exists" ? signupConflict.message : error.code === "clerk_secret_unavailable" ? "account provisioning credential is unavailable" : "account provisioning failed"
3776
3819
  },
3777
3820
  applicationId
3778
3821
  }
@@ -3782,7 +3825,53 @@ async function provisionApplicantForRoute(db, chapter, applicationId, fields) {
3782
3825
  async function provisionApplicant(db, chapter, applicationId, fields) {
3783
3826
  const email = typeof fields.email === "string" ? fields.email : "";
3784
3827
  if (!email) return;
3828
+ if (typeof fields.clerkPrivateMetadataSyncedAt === "number" && typeof fields.clerkUserId === "string") return;
3785
3829
  const stringValue = (value) => typeof value === "string" ? value : void 0;
3830
+ if (chapter.account !== "none") {
3831
+ const secret = await getVaultSecret(db, "clerk_secret_key");
3832
+ if (!secret) throw new AccountProvisioningError("clerk_secret_unavailable", 503);
3833
+ try {
3834
+ const existing = await findSignupAccount(secret, email);
3835
+ if (existing) {
3836
+ if (existing.id !== fields.clerkUserId && !belongsToSignup(existing, applicationId)) {
3837
+ throw new AccountProvisioningError("account_already_exists", 409);
3838
+ }
3839
+ await markPrivateMetadataSynced(db, applicationId, existing.id);
3840
+ return;
3841
+ }
3842
+ const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
3843
+ if (chapter.account === "create") {
3844
+ const result = await createClerkUser(secret, {
3845
+ email,
3846
+ firstName: stringValue(fields.firstName),
3847
+ lastName: stringValue(fields.lastName),
3848
+ privateMetadata
3849
+ });
3850
+ if (!result.ok) throw new AccountProvisioningError(
3851
+ result.existed ? "account_already_exists" : "clerk_account_provisioning_failed",
3852
+ result.existed ? 409 : 502
3853
+ );
3854
+ if (!result.userId) throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
3855
+ await markPrivateMetadataSynced(db, applicationId, result.userId);
3856
+ } else {
3857
+ if (typeof fields.clerkInvitationSentAt === "number") return;
3858
+ const result = await createClerkInvitation(secret, { email });
3859
+ if (!result.ok) throw new AccountProvisioningError(
3860
+ result.existed ? "account_already_exists" : "clerk_account_provisioning_failed",
3861
+ result.existed ? 409 : 502
3862
+ );
3863
+ await db.transact([{
3864
+ t: "update",
3865
+ ns: "applications",
3866
+ id: applicationId,
3867
+ attrs: { clerkInvitationSentAt: Date.now(), clerkAccountPending: false }
3868
+ }]);
3869
+ }
3870
+ } catch (error) {
3871
+ if (error instanceof AccountProvisioningError) throw error;
3872
+ throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
3873
+ }
3874
+ }
3786
3875
  const extra = {};
3787
3876
  for (const field of chapter.application.crmFields) {
3788
3877
  if (fields[field] !== void 0) extra[field] = fields[field];
@@ -3801,44 +3890,9 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
3801
3890
  extra
3802
3891
  }
3803
3892
  );
3893
+ if (chapter.account === "create") await linkProvisionedApplicant(db, chapter.id, applicationId);
3804
3894
  } catch {
3805
3895
  }
3806
- if (chapter.account === "none") return;
3807
- const secret = await getVaultSecret(db, "clerk_secret_key");
3808
- if (!secret) throw new AccountProvisioningError("clerk_secret_unavailable", 503);
3809
- try {
3810
- const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
3811
- if (chapter.account === "create") {
3812
- const result = await createClerkUser(secret, {
3813
- email,
3814
- firstName: stringValue(fields.firstName),
3815
- lastName: stringValue(fields.lastName),
3816
- privateMetadata,
3817
- repairMetadata: {
3818
- publicMetadata: publicProfileRemoval,
3819
- privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
3820
- }
3821
- });
3822
- if (!result.ok || result.existed && !result.refreshed) {
3823
- throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
3824
- }
3825
- await markPrivateMetadataSynced(db, applicationId, result.userId);
3826
- await linkProvisionedApplicant(db, chapter.id, applicationId).catch(() => false);
3827
- } else {
3828
- const result = await createClerkInvitation(secret, { email });
3829
- if (!result.ok) throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
3830
- if (result.existed) {
3831
- const refreshed = await updateClerkUserMetadataByEmail(secret, email, {
3832
- publicMetadata: publicProfileRemoval,
3833
- privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
3834
- });
3835
- if (refreshed) await markPrivateMetadataSynced(db, applicationId);
3836
- }
3837
- }
3838
- } catch (error) {
3839
- if (error instanceof AccountProvisioningError) throw error;
3840
- throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
3841
- }
3842
3896
  }
3843
3897
  async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
3844
3898
  try {
@@ -4012,6 +4066,8 @@ var handleMember = async (req, url, env, ctx) => {
4012
4066
  }
4013
4067
  const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
4014
4068
  const db = ctx.makeDb(env);
4069
+ const signupFailure = await checkSignupEligibility(db, chapter, parsed, submissionId);
4070
+ if (signupFailure) return signupFailure;
4015
4071
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
4016
4072
  const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
4017
4073
  const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
@@ -4030,7 +4086,7 @@ var handleMember = async (req, url, env, ctx) => {
4030
4086
  ...effectiveGrant?.decision === "waive_interview" ? { admissionGrant: effectiveGrant } : {},
4031
4087
  ...giftOffer ? { giftOfferId: giftOffer.id } : {}
4032
4088
  });
4033
- if (!result.ok) return json({ error: result.error }, 400);
4089
+ if (!result.ok) return json({ error: result.error, ...result.code ? { code: result.code } : {} }, result.status ?? 400);
4034
4090
  const apps = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications;
4035
4091
  const canonical2 = Array.isArray(apps) ? apps[0] : void 0;
4036
4092
  if (!canonical2 || canonical2.groupId !== chapter.id) return json({ error: "application state unavailable" }, 503);
@@ -4043,13 +4099,14 @@ var handleMember = async (req, url, env, ctx) => {
4043
4099
  if (normalizeChapterEmail(String(canonical2.email ?? "")) !== normalizeChapterEmail(String(parsed.email ?? ""))) {
4044
4100
  return json({ error: "submission belongs to a different application", code: "application_submission_conflict" }, 409);
4045
4101
  }
4046
- if (!result.duplicate) {
4047
- if (effectiveChapter.sends.adminNotification === "submit") {
4048
- await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical2);
4049
- }
4050
- }
4051
4102
  const provisioningFailure = await provisionApplicantForRoute(db, effectiveChapter, result.id, canonical2);
4052
- if (provisioningFailure) return json(provisioningFailure.body, provisioningFailure.status);
4103
+ if (provisioningFailure) {
4104
+ if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical2.status);
4105
+ return json(provisioningFailure.body, provisioningFailure.status);
4106
+ }
4107
+ if (!result.duplicate && effectiveChapter.sends.adminNotification === "submit") {
4108
+ await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical2);
4109
+ }
4053
4110
  const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
4054
4111
  const group = Array.isArray(groups) ? groups[0] : void 0;
4055
4112
  const state = canonical2 && group ? await continueJoin(db, ctx, env, url.origin, canonical2, group) : null;