@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.
package/dist/index.d.cts CHANGED
@@ -388,7 +388,7 @@ interface LeaderFormationConfig {
388
388
  maxLen?: Record<string, number>;
389
389
  /** Per-string cap when `maxLen` has no field override. Default 4000. */
390
390
  defaultMaxLen?: number;
391
- /** Maximum JSON request size in characters. Default 32768. */
391
+ /** Maximum JSON request size in UTF-8 bytes, enforced while streaming. Default 32768. */
392
392
  bodyCap?: number;
393
393
  }
394
394
  /** Fully resolved formation intake policy. */
@@ -691,7 +691,7 @@ interface ChapterApplication {
691
691
  /** Per-field character cap. Fields not listed use `defaultMaxLen`. */
692
692
  maxLen?: Record<string, number>;
693
693
  defaultMaxLen?: number;
694
- /** Max JSON request body in bytes. Default 32768. */
694
+ /** Max JSON request body in UTF-8 bytes, enforced while streaming. Default 32768. */
695
695
  bodyCap?: number;
696
696
  /** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
697
697
  * writing a row with no consent record. Default `true`. Set `false`
@@ -1230,7 +1230,9 @@ interface ChapterRunbookHints {
1230
1230
  * Build the one CLI-consumable integration for a chapter/hub: the crm_*
1231
1231
  * namespaces + crm_config seed + route probe, merged with the chapter's own
1232
1232
  * namespaces and a guarded `groups`-row seed. Drop it in odla.config.mjs's
1233
- * `integrations` array.
1233
+ * `integrations` array. Claim/private-Clerk policies also seed the exact role
1234
+ * ladder and metadata source for Studio. Seeds are insert-only: when changing
1235
+ * auth policy, reconcile that stored policy explicitly before enabling editing.
1234
1236
  */
1235
1237
  declare function createChapterIntegration(chapter: Chapter, options?: ChapterIntegrationOptions): ChapterIntegrationDescriptor;
1236
1238
 
@@ -1248,7 +1250,7 @@ declare function createChapterIntegration(chapter: Chapter, options?: ChapterInt
1248
1250
  * `superAdminSource: "odla"` uses reserved `$users` and adds no app namespace. A `"claim"`
1249
1251
  * chapter therefore emits the reference namespace set — `applications`,
1250
1252
  * `groups`, `meetings`, `emailLog`, `stripeEventReceipts`,
1251
- * `subscriptionCheckoutIntents`, `superAdmins` — with
1253
+ * `subscriptionCheckoutIntents`, `chapterAccountPolicy`, `superAdmins` — with
1252
1254
  * no `admins` table. */
1253
1255
  declare function chapterDb(mode: ChapterMode, auth: ResolvedAuth, includeNetworkNotes?: boolean, application?: ResolvedApplication, includeSignupControl?: boolean, includeMemberContent?: boolean, includeBuilderLibrary?: boolean, includeLeaderAdmissions?: boolean, includeNetworkPersonRegistry?: boolean, includeNetworkCommercialReadModel?: boolean): {
1254
1256
  schema: DbSchema;
@@ -2567,7 +2569,7 @@ declare function hasDisclaimerAck(fields: Record<string, unknown>): boolean;
2567
2569
  * them. Pure; returns `undefined` when there is nothing to write.
2568
2570
  */
2569
2571
  declare function applicantProfile(chapter: Chapter, fields: Record<string, unknown>): Record<string, unknown> | undefined;
2570
- /** A validated submission, or a 400-worthy validation error the route returns.
2572
+ /** A validated submission, or a validation/duplicate-email error the route returns.
2571
2573
  * `disclaimerAckAt` reports what THIS request recorded — a number when consent
2572
2574
  * was stamped, `null` when none was supplied. It is always present so a missing
2573
2575
  * consent record is visible in the response rather than silently absent from a
@@ -2581,6 +2583,8 @@ type SubmitResult = {
2581
2583
  } | {
2582
2584
  ok: false;
2583
2585
  error: string;
2586
+ code?: string;
2587
+ status?: number;
2584
2588
  };
2585
2589
  /**
2586
2590
  * Submit a membership application (B2 + B3). Validates the configured required
@@ -2590,6 +2594,9 @@ type SubmitResult = {
2590
2594
  * create two applications. The application id is derived from that same stable
2591
2595
  * input, so the duplicate response returns the first request's canonical id
2592
2596
  * rather than a fresh, nonexistent one. Idempotency is package-enforced.
2597
+ * A unique normalized signup email key prevents concurrent submissions with
2598
+ * different ids from creating two applications; a conflict returns status 409.
2599
+ * Apply the current Chapter schema before deploying this writer.
2593
2600
  * `now`/`newId` are injected (deterministic in tests).
2594
2601
  */
2595
2602
  declare function submitApplication(db: ChapterDb, chapter: Chapter, fields: Record<string, unknown>, opts: {
@@ -3072,16 +3079,15 @@ type StripeResult = StripeRequestResult<Record<string, unknown>>;
3072
3079
  * of each re-deriving the auth + encoding. */
3073
3080
  declare function stripeCall(sk: string, method: "GET" | "POST" | "DELETE", path: string, params?: Record<string, unknown>, idempotencyKey?: string): Promise<StripeResult>;
3074
3081
 
3075
- /** The outcome of a Clerk provisioning call. `existed` marks the heal case: Clerk
3076
- * rejected it because the account/invitation is already there, which is the end
3077
- * state we wanted anyway. */
3082
+ /** The outcome of provisioning. An existing identifier is a conflict unless
3083
+ * the account proves it belongs to the original application being retried. */
3078
3084
  interface ClerkResult {
3079
3085
  ok: boolean;
3080
3086
  status: number;
3081
3087
  /** Stable Clerk user id returned by a successful fresh account create. */
3082
3088
  userId?: string;
3083
3089
  existed?: boolean;
3084
- /** Set when an `existed` heal also refreshed the account's metadata. */
3090
+ /** Legacy result field; signup never refreshes an existing account. */
3085
3091
  refreshed?: boolean;
3086
3092
  }
3087
3093
  /** Inputs for a Clerk invitation. */
@@ -3100,9 +3106,8 @@ declare function clerkInviteRequest(input: ClerkInviteInput): {
3100
3106
  path: string;
3101
3107
  body: Record<string, unknown>;
3102
3108
  };
3103
- /** POST the invitation to the Clerk Backend API. A repeat invite for an
3104
- * already-invited email heals to `{ ok: true, existed: true }` rather than
3105
- * reporting a failure the caller would have to special-case. */
3109
+ /** POST a new invitation. Existing identifiers and all validation failures are
3110
+ * rejected; invitations never authorize changes to an existing account. */
3106
3111
  declare function createClerkInvitation(secretKey: string, input: ClerkInviteInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
3107
3112
  /** Inputs for a server-side Clerk user create. */
3108
3113
  interface ClerkUserInput {
@@ -3113,9 +3118,8 @@ interface ClerkUserInput {
3113
3118
  publicMetadata?: Record<string, unknown>;
3114
3119
  /** Backend-only account signals and profile data. */
3115
3120
  privateMetadata?: Record<string, unknown>;
3116
- /** Optional metadata merge used only when create returns 422 for an existing
3117
- * account. This lets migrations remove legacy keys without putting tombstone
3118
- * values on a newly-created user. Defaults to the create metadata. */
3121
+ /** @deprecated Ignored. Use an explicitly authorized metadata update for repairs;
3122
+ * signup must never modify an account found by email. */
3119
3123
  repairMetadata?: ClerkMetadataInput;
3120
3124
  }
3121
3125
  /** Build the Clerk Backend API user-create request. The account is created
@@ -3137,15 +3141,13 @@ declare function updateClerkUserMetadata(secretKey: string, userId: string, inpu
3137
3141
  /** Look a Clerk account up by email and merge its metadata.
3138
3142
  *
3139
3143
  * Returns `false` when no matching user can be resolved or Clerk rejects the
3140
- * lookup or update. Chapter uses this for create-healing and invitation
3141
- * adoption so a repeated application repairs the private account snapshot.
3144
+ * lookup or update. This is an explicit administrative repair primitive;
3145
+ * never call it from public signup or use email knowledge as authorization.
3142
3146
  */
3143
3147
  declare function updateClerkUserMetadataByEmail(secretKey: string, email: string, input: ClerkMetadataInput, fetchImpl?: typeof fetch): Promise<boolean>;
3144
- /** Create the applicant's Clerk account server-side. A repeat for an email that
3145
- * already has an account heals to `{ ok: true, existed: true }` — the account
3146
- * exists, which is the state apply-time provisioning wanted — and refreshes
3147
- * supplied public/private metadata on the existing account so a re-application
3148
- * repairs a previously missed create (`refreshed: true`). */
3148
+ /** Create a new passwordless account. A provider collision never patches the
3149
+ * existing account. A retry may return its id only when backend-private metadata
3150
+ * already names the same original application, preserving all current claims. */
3149
3151
  declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
3150
3152
 
3151
3153
  /** Backend metadata field selected for all role reads and writes. */
package/dist/index.d.ts CHANGED
@@ -388,7 +388,7 @@ interface LeaderFormationConfig {
388
388
  maxLen?: Record<string, number>;
389
389
  /** Per-string cap when `maxLen` has no field override. Default 4000. */
390
390
  defaultMaxLen?: number;
391
- /** Maximum JSON request size in characters. Default 32768. */
391
+ /** Maximum JSON request size in UTF-8 bytes, enforced while streaming. Default 32768. */
392
392
  bodyCap?: number;
393
393
  }
394
394
  /** Fully resolved formation intake policy. */
@@ -691,7 +691,7 @@ interface ChapterApplication {
691
691
  /** Per-field character cap. Fields not listed use `defaultMaxLen`. */
692
692
  maxLen?: Record<string, number>;
693
693
  defaultMaxLen?: number;
694
- /** Max JSON request body in bytes. Default 32768. */
694
+ /** Max JSON request body in UTF-8 bytes, enforced while streaming. Default 32768. */
695
695
  bodyCap?: number;
696
696
  /** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
697
697
  * writing a row with no consent record. Default `true`. Set `false`
@@ -1230,7 +1230,9 @@ interface ChapterRunbookHints {
1230
1230
  * Build the one CLI-consumable integration for a chapter/hub: the crm_*
1231
1231
  * namespaces + crm_config seed + route probe, merged with the chapter's own
1232
1232
  * namespaces and a guarded `groups`-row seed. Drop it in odla.config.mjs's
1233
- * `integrations` array.
1233
+ * `integrations` array. Claim/private-Clerk policies also seed the exact role
1234
+ * ladder and metadata source for Studio. Seeds are insert-only: when changing
1235
+ * auth policy, reconcile that stored policy explicitly before enabling editing.
1234
1236
  */
1235
1237
  declare function createChapterIntegration(chapter: Chapter, options?: ChapterIntegrationOptions): ChapterIntegrationDescriptor;
1236
1238
 
@@ -1248,7 +1250,7 @@ declare function createChapterIntegration(chapter: Chapter, options?: ChapterInt
1248
1250
  * `superAdminSource: "odla"` uses reserved `$users` and adds no app namespace. A `"claim"`
1249
1251
  * chapter therefore emits the reference namespace set — `applications`,
1250
1252
  * `groups`, `meetings`, `emailLog`, `stripeEventReceipts`,
1251
- * `subscriptionCheckoutIntents`, `superAdmins` — with
1253
+ * `subscriptionCheckoutIntents`, `chapterAccountPolicy`, `superAdmins` — with
1252
1254
  * no `admins` table. */
1253
1255
  declare function chapterDb(mode: ChapterMode, auth: ResolvedAuth, includeNetworkNotes?: boolean, application?: ResolvedApplication, includeSignupControl?: boolean, includeMemberContent?: boolean, includeBuilderLibrary?: boolean, includeLeaderAdmissions?: boolean, includeNetworkPersonRegistry?: boolean, includeNetworkCommercialReadModel?: boolean): {
1254
1256
  schema: DbSchema;
@@ -2567,7 +2569,7 @@ declare function hasDisclaimerAck(fields: Record<string, unknown>): boolean;
2567
2569
  * them. Pure; returns `undefined` when there is nothing to write.
2568
2570
  */
2569
2571
  declare function applicantProfile(chapter: Chapter, fields: Record<string, unknown>): Record<string, unknown> | undefined;
2570
- /** A validated submission, or a 400-worthy validation error the route returns.
2572
+ /** A validated submission, or a validation/duplicate-email error the route returns.
2571
2573
  * `disclaimerAckAt` reports what THIS request recorded — a number when consent
2572
2574
  * was stamped, `null` when none was supplied. It is always present so a missing
2573
2575
  * consent record is visible in the response rather than silently absent from a
@@ -2581,6 +2583,8 @@ type SubmitResult = {
2581
2583
  } | {
2582
2584
  ok: false;
2583
2585
  error: string;
2586
+ code?: string;
2587
+ status?: number;
2584
2588
  };
2585
2589
  /**
2586
2590
  * Submit a membership application (B2 + B3). Validates the configured required
@@ -2590,6 +2594,9 @@ type SubmitResult = {
2590
2594
  * create two applications. The application id is derived from that same stable
2591
2595
  * input, so the duplicate response returns the first request's canonical id
2592
2596
  * rather than a fresh, nonexistent one. Idempotency is package-enforced.
2597
+ * A unique normalized signup email key prevents concurrent submissions with
2598
+ * different ids from creating two applications; a conflict returns status 409.
2599
+ * Apply the current Chapter schema before deploying this writer.
2593
2600
  * `now`/`newId` are injected (deterministic in tests).
2594
2601
  */
2595
2602
  declare function submitApplication(db: ChapterDb, chapter: Chapter, fields: Record<string, unknown>, opts: {
@@ -3072,16 +3079,15 @@ type StripeResult = StripeRequestResult<Record<string, unknown>>;
3072
3079
  * of each re-deriving the auth + encoding. */
3073
3080
  declare function stripeCall(sk: string, method: "GET" | "POST" | "DELETE", path: string, params?: Record<string, unknown>, idempotencyKey?: string): Promise<StripeResult>;
3074
3081
 
3075
- /** The outcome of a Clerk provisioning call. `existed` marks the heal case: Clerk
3076
- * rejected it because the account/invitation is already there, which is the end
3077
- * state we wanted anyway. */
3082
+ /** The outcome of provisioning. An existing identifier is a conflict unless
3083
+ * the account proves it belongs to the original application being retried. */
3078
3084
  interface ClerkResult {
3079
3085
  ok: boolean;
3080
3086
  status: number;
3081
3087
  /** Stable Clerk user id returned by a successful fresh account create. */
3082
3088
  userId?: string;
3083
3089
  existed?: boolean;
3084
- /** Set when an `existed` heal also refreshed the account's metadata. */
3090
+ /** Legacy result field; signup never refreshes an existing account. */
3085
3091
  refreshed?: boolean;
3086
3092
  }
3087
3093
  /** Inputs for a Clerk invitation. */
@@ -3100,9 +3106,8 @@ declare function clerkInviteRequest(input: ClerkInviteInput): {
3100
3106
  path: string;
3101
3107
  body: Record<string, unknown>;
3102
3108
  };
3103
- /** POST the invitation to the Clerk Backend API. A repeat invite for an
3104
- * already-invited email heals to `{ ok: true, existed: true }` rather than
3105
- * reporting a failure the caller would have to special-case. */
3109
+ /** POST a new invitation. Existing identifiers and all validation failures are
3110
+ * rejected; invitations never authorize changes to an existing account. */
3106
3111
  declare function createClerkInvitation(secretKey: string, input: ClerkInviteInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
3107
3112
  /** Inputs for a server-side Clerk user create. */
3108
3113
  interface ClerkUserInput {
@@ -3113,9 +3118,8 @@ interface ClerkUserInput {
3113
3118
  publicMetadata?: Record<string, unknown>;
3114
3119
  /** Backend-only account signals and profile data. */
3115
3120
  privateMetadata?: Record<string, unknown>;
3116
- /** Optional metadata merge used only when create returns 422 for an existing
3117
- * account. This lets migrations remove legacy keys without putting tombstone
3118
- * values on a newly-created user. Defaults to the create metadata. */
3121
+ /** @deprecated Ignored. Use an explicitly authorized metadata update for repairs;
3122
+ * signup must never modify an account found by email. */
3119
3123
  repairMetadata?: ClerkMetadataInput;
3120
3124
  }
3121
3125
  /** Build the Clerk Backend API user-create request. The account is created
@@ -3137,15 +3141,13 @@ declare function updateClerkUserMetadata(secretKey: string, userId: string, inpu
3137
3141
  /** Look a Clerk account up by email and merge its metadata.
3138
3142
  *
3139
3143
  * Returns `false` when no matching user can be resolved or Clerk rejects the
3140
- * lookup or update. Chapter uses this for create-healing and invitation
3141
- * adoption so a repeated application repairs the private account snapshot.
3144
+ * lookup or update. This is an explicit administrative repair primitive;
3145
+ * never call it from public signup or use email knowledge as authorization.
3142
3146
  */
3143
3147
  declare function updateClerkUserMetadataByEmail(secretKey: string, email: string, input: ClerkMetadataInput, fetchImpl?: typeof fetch): Promise<boolean>;
3144
- /** Create the applicant's Clerk account server-side. A repeat for an email that
3145
- * already has an account heals to `{ ok: true, existed: true }` — the account
3146
- * exists, which is the state apply-time provisioning wanted — and refreshes
3147
- * supplied public/private metadata on the existing account so a re-application
3148
- * repairs a previously missed create (`refreshed: true`). */
3148
+ /** Create a new passwordless account. A provider collision never patches the
3149
+ * existing account. A retry may return its id only when backend-private metadata
3150
+ * already names the same original application, preserving all current claims. */
3149
3151
  declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
3150
3152
 
3151
3153
  /** Backend metadata field selected for all role reads and writes. */
package/dist/index.js CHANGED
@@ -1,6 +1,13 @@
1
1
  // src/config.ts
2
2
  import { defineCrm } from "@odla-ai/crm";
3
3
 
4
+ // src/schema-account-policy.ts
5
+ var chapterAccountPolicy = { attrs: {
6
+ id: { type: "string", unique: true, indexed: true, optional: false },
7
+ metadata: { type: "string", unique: false, indexed: false, optional: false },
8
+ roles: { type: "json", unique: false, indexed: false, optional: false }
9
+ } };
10
+
4
11
  // src/member.ts
5
12
  import { assertFieldCondition, resolveFieldStates } from "@odla-ai/crm";
6
13
 
@@ -124,8 +131,10 @@ async function submitApplication(db, chapter, fields, opts) {
124
131
  if (fields.focus !== void 0) row2.focus = clampArray(fields.focus, app.maxArrayLen);
125
132
  if (opts.groupId) row2.groupId = opts.groupId;
126
133
  if (opts.runtime) row2.stripeRuntime = opts.runtime;
134
+ if (chapter.account !== "none") row2.clerkAccountPending = true;
127
135
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
128
136
  row2.emailNormalized = normalizeChapterEmail(fields.email);
137
+ row2.signupEmailKey = row2.emailNormalized;
129
138
  }
130
139
  if (opts.admissionGrant?.decision === "waive_interview") {
131
140
  row2.admissionGrantId = opts.admissionGrant.id;
@@ -149,11 +158,21 @@ async function submitApplication(db, chapter, fields, opts) {
149
158
  consumedApplicationId: id10
150
159
  } });
151
160
  }
152
- const { duplicate } = await db.transact(
153
- ops,
154
- opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
155
- );
156
- return { ok: true, id: id10, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
161
+ try {
162
+ const { duplicate } = await db.transact(
163
+ ops,
164
+ opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
165
+ );
166
+ return { ok: true, id: id10, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
167
+ } catch (error) {
168
+ if (error.code === "unique_violation") return {
169
+ ok: false,
170
+ code: "account_already_exists",
171
+ status: 409,
172
+ error: "An account or application already exists for this email. Please sign in to continue."
173
+ };
174
+ throw error;
175
+ }
157
176
  }
158
177
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
159
178
  return {
@@ -228,6 +247,9 @@ var applicationAttrs = {
228
247
  refundPolicyAckAt: attr("number", { optional: true }),
229
248
  prepEmailSentAt: attr("number", { optional: true }),
230
249
  canceled: attr("boolean", { optional: true }),
250
+ signupEmailKey: attr("string", { unique: true, indexed: true, optional: true }),
251
+ clerkInvitationSentAt: attr("number", { optional: true }),
252
+ clerkAccountPending: attr("boolean", { optional: true }),
231
253
  emailNormalized: attr("string", { indexed: true, optional: true }),
232
254
  admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
233
255
  admissionSource: attr("string", { indexed: true, optional: true }),
@@ -900,6 +922,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
900
922
  entities.memberContentLocal = memberContentLocal;
901
923
  }
902
924
  }
925
+ if (auth.source !== "table") entities.chapterAccountPolicy = chapterAccountPolicy;
903
926
  if (auth.source === "table") entities.admins = admins;
904
927
  if (auth.superAdminSource === "table") entities.superAdmins = superAdmins;
905
928
  if (includeNetworkNotes) entities.networkNotes = networkNotes;
@@ -2979,6 +3002,12 @@ function createChapterIntegration(chapter, options = {}) {
2979
3002
  ...emails?.debugEmail ? { debugEmail: emails.debugEmail } : {}
2980
3003
  });
2981
3004
  const seeds = [...crmDesc.seeds ?? []];
3005
+ if (chapter.auth.source !== "table") seeds.push({
3006
+ id: "account-policy",
3007
+ ns: "chapterAccountPolicy",
3008
+ key: { attr: "id", value: chapter.id },
3009
+ attrs: { metadata: chapter.auth.source === "clerk" ? "private" : "public", roles: [...chapter.auth.ladder] }
3010
+ });
2982
3011
  const group = chapter.groupSeed();
2983
3012
  if (group) {
2984
3013
  seeds.push({ id: "group", ns: "groups", key: { attr: "id", value: chapter.id }, attrs: { ...group, createdAt: now } });
@@ -7147,8 +7176,30 @@ async function stripeCall(sk, method, path, params, idempotencyKey) {
7147
7176
  );
7148
7177
  }
7149
7178
 
7179
+ // src/clerk-signup.ts
7180
+ async function findSignupAccount(secret, email, fetchImpl = fetch) {
7181
+ const response = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(normalizeChapterEmail(email))}&limit=2`, {
7182
+ headers: { authorization: `Bearer ${secret}` },
7183
+ redirect: "manual",
7184
+ signal: AbortSignal.timeout(5e3)
7185
+ });
7186
+ if (!response.ok) throw new Error("signup account lookup unavailable");
7187
+ const users = await response.json();
7188
+ if (!Array.isArray(users) || users.length > 1 || users.length === 1 && (typeof users[0]?.id !== "string" || !users[0].id)) {
7189
+ throw new Error("signup account lookup unavailable");
7190
+ }
7191
+ return users[0] ?? null;
7192
+ }
7193
+ function belongsToSignup(account, applicationId) {
7194
+ return typeof applicationId === "string" && applicationId.length > 0 && account.private_metadata?.applicationId === applicationId;
7195
+ }
7196
+
7150
7197
  // src/clerk.ts
7151
- var heal = (status2) => status2 === 422 ? { ok: true, status: status2, existed: true } : { ok: false, status: status2 };
7198
+ async function rejectedCreate(res) {
7199
+ const body = typeof res.json === "function" ? await res.json().catch(() => null) : null;
7200
+ const existed = Array.isArray(body?.errors) && body.errors.some((error) => error?.code === "form_identifier_exists" || error?.code === "duplicate_record");
7201
+ return { ok: false, status: res.status, ...existed ? { existed: true } : {} };
7202
+ }
7152
7203
  function clerkInviteRequest(input) {
7153
7204
  return {
7154
7205
  path: "/v1/invitations",
@@ -7167,7 +7218,7 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
7167
7218
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
7168
7219
  body: JSON.stringify(body)
7169
7220
  });
7170
- return res.ok ? { ok: true, status: res.status } : heal(res.status);
7221
+ return res.ok ? { ok: true, status: res.status } : rejectedCreate(res);
7171
7222
  }
7172
7223
  function clerkUserRequest(input) {
7173
7224
  return {
@@ -7219,20 +7270,10 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
7219
7270
  ...typeof body2?.id === "string" && body2.id ? { userId: body2.id } : {}
7220
7271
  };
7221
7272
  }
7222
- const errorBody = res.status === 400 && typeof res.json === "function" ? await res.json().catch(() => null) : null;
7223
- const existingIdentifier = Array.isArray(errorBody?.errors) && errorBody.errors.some((error) => error.code === "form_identifier_exists");
7224
- const healed = existingIdentifier ? { ok: true, status: res.status, existed: true } : heal(res.status);
7225
- const repair = input.repairMetadata ?? {
7226
- ...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
7227
- ...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
7228
- };
7229
- if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
7230
- const userId = await existingClerkUserId(secretKey, input.email, fetchImpl).catch(() => null);
7231
- const refreshed = userId ? await updateClerkUserMetadata(secretKey, userId, {
7232
- ...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
7233
- ...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
7234
- }, fetchImpl).catch(() => false) : false;
7235
- return { ...healed, refreshed, ...refreshed && userId ? { userId } : {} };
7273
+ const failure = await rejectedCreate(res);
7274
+ if (!failure.existed) return failure;
7275
+ const account = await findSignupAccount(secretKey, input.email, fetchImpl).catch(() => null);
7276
+ return account && belongsToSignup(account, input.privateMetadata?.applicationId) ? { ok: true, status: res.status, existed: true, userId: account.id } : failure;
7236
7277
  }
7237
7278
 
7238
7279
  // src/clerk-roles.ts