@odla-ai/chapter 0.23.0 → 0.25.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.
package/dist/index.d.cts CHANGED
@@ -21,7 +21,14 @@ interface ChapterCopy {
21
21
  join: {
22
22
  form: TextFields<"submit" | "submitting" | "submitFailed" | "unexpectedFailure">;
23
23
  booking: TextFields<"unavailable" | "loadFailed" | "slotTaken" | "failed" | "loading" | "book" | "booking">;
24
- payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete">;
24
+ /**
25
+ * `consent` is the affirmative statement a member agrees to before the card
26
+ * form appears, rendered beside the checkbox and beneath the policy text.
27
+ * It is what the member is shown at the moment of consent, so it says they
28
+ * agree rather than merely restating the policy. Set it to an empty string
29
+ * to render the checkbox with the policy as its only label.
30
+ */
31
+ payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete" | "consent">;
25
32
  done: TextFields<"label" | "calendarInvite" | "memberArea">;
26
33
  };
27
34
  members: {
@@ -320,10 +327,9 @@ interface ChapterApplication {
320
327
  * writing a row with no consent record. Default `true`. Set `false`
321
328
  * deliberately only when the site renders no consent control. */
322
329
  requireDisclaimerAck?: boolean;
323
- /** Allowlist of fields that reach the Clerk account's client-readable
324
- * `public_metadata.profile`. Default `[]`, so application details remain
325
- * db-only. Set a curated list (e.g. `["phone", "state", "focus"]`) for
326
- * fields the browser may read. */
330
+ /** Allowlist of application fields mirrored into the Clerk account's
331
+ * backend-only `private_metadata.profile`. Default `[]`. Use this for small
332
+ * account/admin signals; the application row and CRM remain canonical. */
327
333
  profileFields?: readonly string[];
328
334
  /** Extra application fields carried into the one-way CRM projection, on top of
329
335
  * the built-in identity/contact set. Each MUST be declared on your crm person
@@ -521,6 +527,20 @@ interface ChapterIntegrationDescriptor {
521
527
  path: string;
522
528
  expectedStatus: number;
523
529
  }>;
530
+ /** Runbooks that bear on THIS chapter's role, so tooling can point at the
531
+ * right procedure without the operator knowing it exists. A chapter declaring
532
+ * `network.targets` is a leader hub; one declaring none is a follower site,
533
+ * and the two follow different procedures for the same words ("connect",
534
+ * "share records"). */
535
+ runbooks: ChapterRunbookHints;
536
+ }
537
+ /** Which runbooks apply, and why. */
538
+ interface ChapterRunbookHints {
539
+ role: "leader" | "follower";
540
+ /** Platform runbook slugs, most relevant first. */
541
+ slugs: string[];
542
+ /** One line a human or agent can act on. */
543
+ guidance: string;
524
544
  }
525
545
  /**
526
546
  * Build the one CLI-consumable integration for a chapter/hub: the crm_*
@@ -856,11 +876,10 @@ declare function clampArray(value: unknown, max: number): unknown;
856
876
  * the boolean an API client sends and the string a plain HTML form posts. */
857
877
  declare function hasDisclaimerAck(fields: Record<string, unknown>): boolean;
858
878
  /**
859
- * The applicant profile written to the Clerk account's client-readable
860
- * `public_metadata.profile`. Projects each configured non-identity field, plus
861
- * `focus` (clamped), but ONLY those in `application.profileFields`, so a site
862
- * keeps confidential fields (`message`, `referral`) db-only. Derived from
863
- * config, so a site's own field names project without this package knowing
879
+ * The applicant profile written to the Clerk account's backend-only
880
+ * `private_metadata.profile`. Projects each configured non-identity field, plus
881
+ * `focus` (clamped), but ONLY those in `application.profileFields`. Derived
882
+ * from config, so a site's own field names project without this package knowing
864
883
  * them. Pure; returns `undefined` when there is nothing to write.
865
884
  */
866
885
  declare function applicantProfile(chapter: Chapter, fields: Record<string, unknown>): Record<string, unknown> | undefined;
@@ -1076,7 +1095,7 @@ interface ClerkResult {
1076
1095
  ok: boolean;
1077
1096
  status: number;
1078
1097
  existed?: boolean;
1079
- /** Set when an `existed` heal also refreshed the account's public_metadata. */
1098
+ /** Set when an `existed` heal also refreshed the account's metadata. */
1080
1099
  refreshed?: boolean;
1081
1100
  }
1082
1101
  /** Inputs for a Clerk invitation. */
@@ -1084,8 +1103,9 @@ interface ClerkInviteInput {
1084
1103
  email: string;
1085
1104
  /** Where the accept-invitation link lands (usually the member area). */
1086
1105
  redirectUrl?: string;
1087
- /** Written to the invitation's `public_metadata`, so the accepted account
1088
- * carries your own profile fields. */
1106
+ /** Written to the invitation's `public_metadata`. Clerk application
1107
+ * invitations cannot carry private metadata, so never put applicant profile
1108
+ * fields here. Chapter completes that backend-only write after acceptance. */
1089
1109
  publicMetadata?: Record<string, unknown>;
1090
1110
  }
1091
1111
  /** Build the Clerk Backend API invitation request (path + JSON body). Pure, so
@@ -1103,9 +1123,14 @@ interface ClerkUserInput {
1103
1123
  email: string;
1104
1124
  firstName?: string;
1105
1125
  lastName?: string;
1106
- /** Written to the user's `public_metadata` — the site's own profile fields
1107
- * (role, tier, whatever the member area reads). */
1126
+ /** Backend-authored, browser-readable claims such as `role`. */
1108
1127
  publicMetadata?: Record<string, unknown>;
1128
+ /** Backend-only account signals and profile data. */
1129
+ privateMetadata?: Record<string, unknown>;
1130
+ /** Optional metadata merge used only when create returns 422 for an existing
1131
+ * account. This lets migrations remove legacy keys without putting tombstone
1132
+ * values on a newly-created user. Defaults to the create metadata. */
1133
+ repairMetadata?: ClerkMetadataInput;
1109
1134
  }
1110
1135
  /** Build the Clerk Backend API user-create request. The account is created
1111
1136
  * passwordless (the member signs in via the site's Clerk flow), so join step 3
@@ -1114,21 +1139,41 @@ declare function clerkUserRequest(input: ClerkUserInput): {
1114
1139
  path: string;
1115
1140
  body: Record<string, unknown>;
1116
1141
  };
1142
+ /** One merge-PATCH to the metadata fields Clerk exposes on its Backend API. */
1143
+ interface ClerkMetadataInput {
1144
+ publicMetadata?: Record<string, unknown>;
1145
+ privateMetadata?: Record<string, unknown>;
1146
+ }
1147
+ /** Merge metadata into an existing Clerk user. Nested `null` values remove old
1148
+ * keys, which Chapter uses to migrate its former public profile into private
1149
+ * metadata without disturbing the public `role` claim. */
1150
+ declare function updateClerkUserMetadata(secretKey: string, userId: string, input: ClerkMetadataInput, fetchImpl?: typeof fetch): Promise<boolean>;
1151
+ /** Look a Clerk account up by email and merge its metadata.
1152
+ *
1153
+ * Returns `false` when no matching user can be resolved or Clerk rejects the
1154
+ * lookup or update. Chapter uses this for create-healing and invitation
1155
+ * adoption so a repeated application repairs the private account snapshot.
1156
+ */
1157
+ declare function updateClerkUserMetadataByEmail(secretKey: string, email: string, input: ClerkMetadataInput, fetchImpl?: typeof fetch): Promise<boolean>;
1117
1158
  /** Create the applicant's Clerk account server-side. A repeat for an email that
1118
1159
  * already has an account heals to `{ ok: true, existed: true }` — the account
1119
- * exists, which is the state apply-time provisioning wanted — and, when
1120
- * `publicMetadata` was supplied, refreshes it on the existing account so a
1121
- * re-application repairs a previously missed create (`refreshed: true`). */
1160
+ * exists, which is the state apply-time provisioning wanted — and refreshes
1161
+ * supplied public/private metadata on the existing account so a re-application
1162
+ * repairs a previously missed create (`refreshed: true`). */
1122
1163
  declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
1123
1164
 
1124
1165
  /** A Clerk user as chapter's role layer sees it. `role` defaults to the lowest
1125
- * rung when `public_metadata` carries none; `publicMetadata` is returned raw so a
1126
- * site with a custom ladder can re-derive it. */
1166
+ * rung when `public_metadata` carries none. Raw public/private metadata is
1167
+ * returned only from this backend helper; `profile` and `applicationId` are the
1168
+ * normalized Chapter-owned private signals. */
1127
1169
  interface ClerkUserRecord {
1128
1170
  id: string;
1129
1171
  email?: string;
1130
1172
  role: string;
1131
1173
  publicMetadata: Record<string, unknown>;
1174
+ privateMetadata: Record<string, unknown>;
1175
+ profile: Record<string, unknown>;
1176
+ applicationId?: string;
1132
1177
  }
1133
1178
  /** Look a Clerk user up by email. `null` when no such user — or when the lookup
1134
1179
  * fails (a role gate treats an unresolvable user as absent, matching the site's
@@ -1143,8 +1188,9 @@ declare function clerkGetUser(secretKey: string, id: string, fetchImpl?: typeof
1143
1188
  * until a short page. A page fetch that fails THROWS rather than returning a
1144
1189
  * partial list, so the caller never mistakes a truncated roster for the whole. */
1145
1190
  declare function clerkListUsers(secretKey: string, fetchImpl?: typeof fetch): Promise<ClerkUserRecord[]>;
1146
- /** Write a user's role: a MERGE-PATCH of only `{ role }` on `public_metadata`, so
1147
- * it leaves a separately-written `profile` untouched. Returns whether it stuck. */
1191
+ /** Write a user's role: a MERGE-PATCH of only `{ role }` on `public_metadata`.
1192
+ * The backend-only profile lives in `private_metadata`, so the two signals
1193
+ * cannot overwrite each other. Returns whether the role write stuck. */
1148
1194
  declare function clerkSetRole(secretKey: string, id: string, role: string, fetchImpl?: typeof fetch): Promise<boolean>;
1149
1195
 
1150
1196
  /** Which auth mode a secret is required for: `"client"` (publishable key only) or
@@ -1210,8 +1256,9 @@ interface IntegrationDescriptor {
1210
1256
  * the one public `setting` (the `pk_*` publishable key, served to the SPA), the
1211
1257
  * `secrets` auth mode `"full"` needs in the tenant vault, BOTH sync directions —
1212
1258
  * the inbound `$users` webhook mirror (`provider->odla`) and the outbound
1213
- * role/profile write via `clerk_secret_key` (`odla->provider`, chapter's
1214
- * `clerk.ts`) — and the `provision` split. Data only; it performs none of it.
1259
+ * public role/private profile write via `clerk_secret_key` (`odla->provider`,
1260
+ * chapter's `clerk.ts`) — and the `provision` split. Data only; it performs
1261
+ * none of it.
1215
1262
  */
1216
1263
  declare const clerkIntegration: IntegrationDescriptor;
1217
1264
 
@@ -1470,4 +1517,4 @@ type ApplicationBookingPatch = {
1470
1517
  * already there (never backward). */
1471
1518
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
1472
1519
 
1473
- export { type AccountModel, type AdminNotificationTrigger, type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterCopy, type ChapterCopyInput, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterNetwork, type ChapterNetworkTarget, type ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type CompiledChapterBrandTokens, DEFAULT_CHAPTER_COPY, DEFAULT_SHARE_FIELDS, type DashboardMetricData, type DashboardMetricSeries, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, type JoinConfigGroup, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, type ResolvedApplication, type ResolvedAuth, type ResolvedNetwork, type ResolvedNetworkTarget, type ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedBusiness, type SharedPerson, type SharedRecord, type StripeEvent, type StripeResult, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterBrandFromTokens, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, dashboardMetricData, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, formatChapterCopy, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, networkSourceTag, normalizeSharedRecord, normalizeWebhookEvent, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolveChapterCopy, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, sharedRecordFromCrm, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, validateScheduling, verifyStripeSignature, webhookMutationId };
1520
+ export { type AccountModel, type AdminNotificationTrigger, type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterCopy, type ChapterCopyInput, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterNetwork, type ChapterNetworkTarget, type ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterRunbookHints, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkMetadataInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type CompiledChapterBrandTokens, DEFAULT_CHAPTER_COPY, DEFAULT_SHARE_FIELDS, type DashboardMetricData, type DashboardMetricSeries, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, type JoinConfigGroup, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, type ResolvedApplication, type ResolvedAuth, type ResolvedNetwork, type ResolvedNetworkTarget, type ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedBusiness, type SharedPerson, type SharedRecord, type StripeEvent, type StripeResult, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterBrandFromTokens, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, dashboardMetricData, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, formatChapterCopy, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, networkSourceTag, normalizeSharedRecord, normalizeWebhookEvent, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolveChapterCopy, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, sharedRecordFromCrm, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, updateClerkUserMetadata, updateClerkUserMetadataByEmail, validateScheduling, verifyStripeSignature, webhookMutationId };
package/dist/index.d.ts CHANGED
@@ -21,7 +21,14 @@ interface ChapterCopy {
21
21
  join: {
22
22
  form: TextFields<"submit" | "submitting" | "submitFailed" | "unexpectedFailure">;
23
23
  booking: TextFields<"unavailable" | "loadFailed" | "slotTaken" | "failed" | "loading" | "book" | "booking">;
24
- payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete">;
24
+ /**
25
+ * `consent` is the affirmative statement a member agrees to before the card
26
+ * form appears, rendered beside the checkbox and beneath the policy text.
27
+ * It is what the member is shown at the moment of consent, so it says they
28
+ * agree rather than merely restating the policy. Set it to an empty string
29
+ * to render the checkbox with the policy as its only label.
30
+ */
31
+ payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete" | "consent">;
25
32
  done: TextFields<"label" | "calendarInvite" | "memberArea">;
26
33
  };
27
34
  members: {
@@ -320,10 +327,9 @@ interface ChapterApplication {
320
327
  * writing a row with no consent record. Default `true`. Set `false`
321
328
  * deliberately only when the site renders no consent control. */
322
329
  requireDisclaimerAck?: boolean;
323
- /** Allowlist of fields that reach the Clerk account's client-readable
324
- * `public_metadata.profile`. Default `[]`, so application details remain
325
- * db-only. Set a curated list (e.g. `["phone", "state", "focus"]`) for
326
- * fields the browser may read. */
330
+ /** Allowlist of application fields mirrored into the Clerk account's
331
+ * backend-only `private_metadata.profile`. Default `[]`. Use this for small
332
+ * account/admin signals; the application row and CRM remain canonical. */
327
333
  profileFields?: readonly string[];
328
334
  /** Extra application fields carried into the one-way CRM projection, on top of
329
335
  * the built-in identity/contact set. Each MUST be declared on your crm person
@@ -521,6 +527,20 @@ interface ChapterIntegrationDescriptor {
521
527
  path: string;
522
528
  expectedStatus: number;
523
529
  }>;
530
+ /** Runbooks that bear on THIS chapter's role, so tooling can point at the
531
+ * right procedure without the operator knowing it exists. A chapter declaring
532
+ * `network.targets` is a leader hub; one declaring none is a follower site,
533
+ * and the two follow different procedures for the same words ("connect",
534
+ * "share records"). */
535
+ runbooks: ChapterRunbookHints;
536
+ }
537
+ /** Which runbooks apply, and why. */
538
+ interface ChapterRunbookHints {
539
+ role: "leader" | "follower";
540
+ /** Platform runbook slugs, most relevant first. */
541
+ slugs: string[];
542
+ /** One line a human or agent can act on. */
543
+ guidance: string;
524
544
  }
525
545
  /**
526
546
  * Build the one CLI-consumable integration for a chapter/hub: the crm_*
@@ -856,11 +876,10 @@ declare function clampArray(value: unknown, max: number): unknown;
856
876
  * the boolean an API client sends and the string a plain HTML form posts. */
857
877
  declare function hasDisclaimerAck(fields: Record<string, unknown>): boolean;
858
878
  /**
859
- * The applicant profile written to the Clerk account's client-readable
860
- * `public_metadata.profile`. Projects each configured non-identity field, plus
861
- * `focus` (clamped), but ONLY those in `application.profileFields`, so a site
862
- * keeps confidential fields (`message`, `referral`) db-only. Derived from
863
- * config, so a site's own field names project without this package knowing
879
+ * The applicant profile written to the Clerk account's backend-only
880
+ * `private_metadata.profile`. Projects each configured non-identity field, plus
881
+ * `focus` (clamped), but ONLY those in `application.profileFields`. Derived
882
+ * from config, so a site's own field names project without this package knowing
864
883
  * them. Pure; returns `undefined` when there is nothing to write.
865
884
  */
866
885
  declare function applicantProfile(chapter: Chapter, fields: Record<string, unknown>): Record<string, unknown> | undefined;
@@ -1076,7 +1095,7 @@ interface ClerkResult {
1076
1095
  ok: boolean;
1077
1096
  status: number;
1078
1097
  existed?: boolean;
1079
- /** Set when an `existed` heal also refreshed the account's public_metadata. */
1098
+ /** Set when an `existed` heal also refreshed the account's metadata. */
1080
1099
  refreshed?: boolean;
1081
1100
  }
1082
1101
  /** Inputs for a Clerk invitation. */
@@ -1084,8 +1103,9 @@ interface ClerkInviteInput {
1084
1103
  email: string;
1085
1104
  /** Where the accept-invitation link lands (usually the member area). */
1086
1105
  redirectUrl?: string;
1087
- /** Written to the invitation's `public_metadata`, so the accepted account
1088
- * carries your own profile fields. */
1106
+ /** Written to the invitation's `public_metadata`. Clerk application
1107
+ * invitations cannot carry private metadata, so never put applicant profile
1108
+ * fields here. Chapter completes that backend-only write after acceptance. */
1089
1109
  publicMetadata?: Record<string, unknown>;
1090
1110
  }
1091
1111
  /** Build the Clerk Backend API invitation request (path + JSON body). Pure, so
@@ -1103,9 +1123,14 @@ interface ClerkUserInput {
1103
1123
  email: string;
1104
1124
  firstName?: string;
1105
1125
  lastName?: string;
1106
- /** Written to the user's `public_metadata` — the site's own profile fields
1107
- * (role, tier, whatever the member area reads). */
1126
+ /** Backend-authored, browser-readable claims such as `role`. */
1108
1127
  publicMetadata?: Record<string, unknown>;
1128
+ /** Backend-only account signals and profile data. */
1129
+ privateMetadata?: Record<string, unknown>;
1130
+ /** Optional metadata merge used only when create returns 422 for an existing
1131
+ * account. This lets migrations remove legacy keys without putting tombstone
1132
+ * values on a newly-created user. Defaults to the create metadata. */
1133
+ repairMetadata?: ClerkMetadataInput;
1109
1134
  }
1110
1135
  /** Build the Clerk Backend API user-create request. The account is created
1111
1136
  * passwordless (the member signs in via the site's Clerk flow), so join step 3
@@ -1114,21 +1139,41 @@ declare function clerkUserRequest(input: ClerkUserInput): {
1114
1139
  path: string;
1115
1140
  body: Record<string, unknown>;
1116
1141
  };
1142
+ /** One merge-PATCH to the metadata fields Clerk exposes on its Backend API. */
1143
+ interface ClerkMetadataInput {
1144
+ publicMetadata?: Record<string, unknown>;
1145
+ privateMetadata?: Record<string, unknown>;
1146
+ }
1147
+ /** Merge metadata into an existing Clerk user. Nested `null` values remove old
1148
+ * keys, which Chapter uses to migrate its former public profile into private
1149
+ * metadata without disturbing the public `role` claim. */
1150
+ declare function updateClerkUserMetadata(secretKey: string, userId: string, input: ClerkMetadataInput, fetchImpl?: typeof fetch): Promise<boolean>;
1151
+ /** Look a Clerk account up by email and merge its metadata.
1152
+ *
1153
+ * Returns `false` when no matching user can be resolved or Clerk rejects the
1154
+ * lookup or update. Chapter uses this for create-healing and invitation
1155
+ * adoption so a repeated application repairs the private account snapshot.
1156
+ */
1157
+ declare function updateClerkUserMetadataByEmail(secretKey: string, email: string, input: ClerkMetadataInput, fetchImpl?: typeof fetch): Promise<boolean>;
1117
1158
  /** Create the applicant's Clerk account server-side. A repeat for an email that
1118
1159
  * already has an account heals to `{ ok: true, existed: true }` — the account
1119
- * exists, which is the state apply-time provisioning wanted — and, when
1120
- * `publicMetadata` was supplied, refreshes it on the existing account so a
1121
- * re-application repairs a previously missed create (`refreshed: true`). */
1160
+ * exists, which is the state apply-time provisioning wanted — and refreshes
1161
+ * supplied public/private metadata on the existing account so a re-application
1162
+ * repairs a previously missed create (`refreshed: true`). */
1122
1163
  declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
1123
1164
 
1124
1165
  /** A Clerk user as chapter's role layer sees it. `role` defaults to the lowest
1125
- * rung when `public_metadata` carries none; `publicMetadata` is returned raw so a
1126
- * site with a custom ladder can re-derive it. */
1166
+ * rung when `public_metadata` carries none. Raw public/private metadata is
1167
+ * returned only from this backend helper; `profile` and `applicationId` are the
1168
+ * normalized Chapter-owned private signals. */
1127
1169
  interface ClerkUserRecord {
1128
1170
  id: string;
1129
1171
  email?: string;
1130
1172
  role: string;
1131
1173
  publicMetadata: Record<string, unknown>;
1174
+ privateMetadata: Record<string, unknown>;
1175
+ profile: Record<string, unknown>;
1176
+ applicationId?: string;
1132
1177
  }
1133
1178
  /** Look a Clerk user up by email. `null` when no such user — or when the lookup
1134
1179
  * fails (a role gate treats an unresolvable user as absent, matching the site's
@@ -1143,8 +1188,9 @@ declare function clerkGetUser(secretKey: string, id: string, fetchImpl?: typeof
1143
1188
  * until a short page. A page fetch that fails THROWS rather than returning a
1144
1189
  * partial list, so the caller never mistakes a truncated roster for the whole. */
1145
1190
  declare function clerkListUsers(secretKey: string, fetchImpl?: typeof fetch): Promise<ClerkUserRecord[]>;
1146
- /** Write a user's role: a MERGE-PATCH of only `{ role }` on `public_metadata`, so
1147
- * it leaves a separately-written `profile` untouched. Returns whether it stuck. */
1191
+ /** Write a user's role: a MERGE-PATCH of only `{ role }` on `public_metadata`.
1192
+ * The backend-only profile lives in `private_metadata`, so the two signals
1193
+ * cannot overwrite each other. Returns whether the role write stuck. */
1148
1194
  declare function clerkSetRole(secretKey: string, id: string, role: string, fetchImpl?: typeof fetch): Promise<boolean>;
1149
1195
 
1150
1196
  /** Which auth mode a secret is required for: `"client"` (publishable key only) or
@@ -1210,8 +1256,9 @@ interface IntegrationDescriptor {
1210
1256
  * the one public `setting` (the `pk_*` publishable key, served to the SPA), the
1211
1257
  * `secrets` auth mode `"full"` needs in the tenant vault, BOTH sync directions —
1212
1258
  * the inbound `$users` webhook mirror (`provider->odla`) and the outbound
1213
- * role/profile write via `clerk_secret_key` (`odla->provider`, chapter's
1214
- * `clerk.ts`) — and the `provision` split. Data only; it performs none of it.
1259
+ * public role/private profile write via `clerk_secret_key` (`odla->provider`,
1260
+ * chapter's `clerk.ts`) — and the `provision` split. Data only; it performs
1261
+ * none of it.
1215
1262
  */
1216
1263
  declare const clerkIntegration: IntegrationDescriptor;
1217
1264
 
@@ -1470,4 +1517,4 @@ type ApplicationBookingPatch = {
1470
1517
  * already there (never backward). */
1471
1518
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
1472
1519
 
1473
- export { type AccountModel, type AdminNotificationTrigger, type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterCopy, type ChapterCopyInput, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterNetwork, type ChapterNetworkTarget, type ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type CompiledChapterBrandTokens, DEFAULT_CHAPTER_COPY, DEFAULT_SHARE_FIELDS, type DashboardMetricData, type DashboardMetricSeries, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, type JoinConfigGroup, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, type ResolvedApplication, type ResolvedAuth, type ResolvedNetwork, type ResolvedNetworkTarget, type ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedBusiness, type SharedPerson, type SharedRecord, type StripeEvent, type StripeResult, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterBrandFromTokens, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, dashboardMetricData, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, formatChapterCopy, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, networkSourceTag, normalizeSharedRecord, normalizeWebhookEvent, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolveChapterCopy, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, sharedRecordFromCrm, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, validateScheduling, verifyStripeSignature, webhookMutationId };
1520
+ export { type AccountModel, type AdminNotificationTrigger, type Applicant, type ApplicationBookingPatch, type ApplicationRecord, type ApplicationSummary, type Attr, type AttrType, type Chapter, type ChapterApplication, type ChapterAuth, type ChapterBrand, type ChapterConfig, type ChapterCopy, type ChapterCopyInput, type ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterNetwork, type ChapterNetworkTarget, type ChapterOperations, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterRunbookHints, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkMetadataInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, type CompiledChapterBrandTokens, DEFAULT_CHAPTER_COPY, DEFAULT_SHARE_FIELDS, type DashboardMetricData, type DashboardMetricSeries, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, type IntegrationDescriptor, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type IntegrationSync, type JoinConfigGroup, type LiveEvent, type MailSender, type MeetingForReconcile, type MeetingRecord, type MeetingReschedulePatch, type MemberApplication, type MemberSession, type NewMeetingRow, type NotifyDeps, type NotifyInput, type NotifyResult, type PaymentsGroup, type ProjectionDeps, type ReconcileDecision, type ResolvedApplication, type ResolvedAuth, type ResolvedNetwork, type ResolvedNetworkTarget, type ResolvedOperations, type ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedBusiness, type SharedPerson, type SharedRecord, type StripeEvent, type StripeResult, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, backfillCrm, bookingDecision, brandTokens, bucketSeries, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterBrandFromTokens, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, dashboardMetricData, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, formatChapterCopy, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, networkSourceTag, normalizeSharedRecord, normalizeWebhookEvent, paymentsReady, personInputFromApp, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolveChapterCopy, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, sharedRecordFromCrm, slotWindow, stageIndex, stripeCall, stripeForm, subAnnualCents, submitApplication, subscriptionIdempotencyKey, syncApplicationToCrm, updateClerkUserMetadata, updateClerkUserMetadataByEmail, validateScheduling, verifyStripeSignature, webhookMutationId };
package/dist/index.js CHANGED
@@ -44,6 +44,10 @@ var applications = {
44
44
  meetingAt: attr("number", { indexed: true, optional: true }),
45
45
  meetingLink: attr("string", { optional: true }),
46
46
  clerkUserId: attr("string", { indexed: true, optional: true }),
47
+ // One-time/effect-repair marker. Absent means /api/me should copy the
48
+ // allowlisted application snapshot into Clerk private_metadata and remove
49
+ // the former public_metadata copy.
50
+ clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
47
51
  phone: attr("string", { optional: true }),
48
52
  state: attr("string", { optional: true }),
49
53
  groupId: attr("string", { indexed: true, optional: true }),
@@ -687,7 +691,8 @@ var DEFAULT_JOIN_COPY = {
687
691
  pending: "Confirming your payment\u2026",
688
692
  processing: "Processing\u2026",
689
693
  payAndContinue: "Pay and continue",
690
- incomplete: "The payment did not complete. Please try again."
694
+ incomplete: "The payment did not complete. Please try again.",
695
+ consent: "I have read and agree to the pricing and refund policy above."
691
696
  },
692
697
  done: {
693
698
  label: "You're booked",
@@ -949,6 +954,16 @@ function createChapterIntegration(chapter, options = {}) {
949
954
  if (group) {
950
955
  seeds.push({ id: "group", ns: "groups", key: { attr: "id", value: chapter.id }, attrs: { ...group, createdAt: now } });
951
956
  }
957
+ const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
958
+ const runbooks = isLeader ? {
959
+ role: "leader",
960
+ slugs: ["chapter-leader", "chapter-network", "crm"],
961
+ guidance: `${chapter.name} is a LEADER hub: it holds the CRM and shares records with its chapters. Read it with \`odla-ai runbook get chapter-leader\`.`
962
+ } : {
963
+ role: "follower",
964
+ slugs: ["chapter-follower", "chapter-network"],
965
+ guidance: `${chapter.name} is a FOLLOWER site: it receives people and records from a leader hub. Read it with \`odla-ai runbook get chapter-follower\`.`
966
+ };
952
967
  return {
953
968
  id: "chapter",
954
969
  title: `Chapter: ${chapter.name}`,
@@ -959,7 +974,8 @@ function createChapterIntegration(chapter, options = {}) {
959
974
  },
960
975
  rules: { ...crmDesc.rules, ...chapter.rules },
961
976
  seeds,
962
- probes: [...crmDesc.probes ?? []]
977
+ probes: [...crmDesc.probes ?? []],
978
+ runbooks
963
979
  };
964
980
  }
965
981
 
@@ -1346,7 +1362,7 @@ function billingColumns(app) {
1346
1362
  async function syncApplicationToCrm(deps, opts) {
1347
1363
  const emailKey = str(opts.app.email).toLowerCase();
1348
1364
  if (!emailKey) return null;
1349
- const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1365
+ const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1350
1366
  const input = personInputFromApp(deps.chapter, opts.app);
1351
1367
  const { crm_record } = await deps.db.query({
1352
1368
  crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
@@ -1356,16 +1372,16 @@ async function syncApplicationToCrm(deps, opts) {
1356
1372
  let recordId;
1357
1373
  if (existing && typeof existing.id === "string") {
1358
1374
  recordId = existing.id;
1359
- await updateRecord2(crmDeps, { id: recordId, input });
1375
+ await updateRecord2(recordDeps, { id: recordId, input });
1360
1376
  } else {
1361
- const created = await createRecord2(crmDeps, { type: "person", input, ...stage ? { stage } : {} });
1377
+ const created = await createRecord2(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
1362
1378
  recordId = created.id;
1363
1379
  }
1364
1380
  if (existing && stage && existing.stage !== stage) {
1365
- await setStage(crmDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
1381
+ await setStage(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
1366
1382
  }
1367
1383
  await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
1368
- await linkIdentity(crmDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1384
+ await linkIdentity(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1369
1385
  return recordId;
1370
1386
  }
1371
1387
  async function backfillCrm(deps) {
@@ -1490,23 +1506,30 @@ function clerkUserRequest(input) {
1490
1506
  skip_password_requirement: true,
1491
1507
  ...input.firstName ? { first_name: input.firstName } : {},
1492
1508
  ...input.lastName ? { last_name: input.lastName } : {},
1493
- ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
1509
+ ...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
1510
+ ...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
1494
1511
  }
1495
1512
  };
1496
1513
  }
1497
- async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
1514
+ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fetch) {
1515
+ const res = await fetchImpl(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}/metadata`, {
1516
+ method: "PATCH",
1517
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
1518
+ body: JSON.stringify({
1519
+ ...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
1520
+ ...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
1521
+ })
1522
+ });
1523
+ return res.ok;
1524
+ }
1525
+ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
1498
1526
  const auth = { authorization: `Bearer ${secretKey}` };
1499
1527
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
1500
1528
  if (!found.ok) return false;
1501
1529
  const users = await found.json().catch(() => null);
1502
1530
  const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
1503
1531
  if (!id2) return false;
1504
- const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id2}/metadata`, {
1505
- method: "PATCH",
1506
- headers: { ...auth, "content-type": "application/json" },
1507
- body: JSON.stringify({ public_metadata: publicMetadata })
1508
- });
1509
- return patched.ok;
1532
+ return updateClerkUserMetadata(secretKey, id2, input, fetchImpl);
1510
1533
  }
1511
1534
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
1512
1535
  const { path, body } = clerkUserRequest(input);
@@ -1517,8 +1540,15 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
1517
1540
  });
1518
1541
  if (res.ok) return { ok: true, status: res.status };
1519
1542
  const healed = heal(res.status);
1520
- if (!healed.existed || !input.publicMetadata) return healed;
1521
- const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
1543
+ const repair = input.repairMetadata ?? {
1544
+ ...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
1545
+ ...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
1546
+ };
1547
+ if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
1548
+ const refreshed = await updateClerkUserMetadataByEmail(secretKey, input.email, {
1549
+ ...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
1550
+ ...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
1551
+ }, fetchImpl).catch(() => false);
1522
1552
  return { ...healed, refreshed };
1523
1553
  }
1524
1554
 
@@ -1529,9 +1559,21 @@ var PAGE = 100;
1529
1559
  function toRecord(u) {
1530
1560
  if (typeof u.id !== "string") return null;
1531
1561
  const pm = u.public_metadata ?? {};
1562
+ const privateMetadata = u.private_metadata ?? {};
1563
+ const rawProfile = privateMetadata.profile;
1564
+ const profile = rawProfile && typeof rawProfile === "object" && !Array.isArray(rawProfile) ? rawProfile : {};
1565
+ const applicationId = typeof privateMetadata.applicationId === "string" ? privateMetadata.applicationId : void 0;
1532
1566
  const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
1533
1567
  const email = u.email_addresses?.[0]?.email_address;
1534
- return { id: u.id, email: typeof email === "string" ? email : void 0, role, publicMetadata: pm };
1568
+ return {
1569
+ id: u.id,
1570
+ email: typeof email === "string" ? email : void 0,
1571
+ role,
1572
+ publicMetadata: pm,
1573
+ privateMetadata,
1574
+ profile,
1575
+ ...applicationId ? { applicationId } : {}
1576
+ };
1535
1577
  }
1536
1578
  async function clerkGet(path, secretKey, fetchImpl) {
1537
1579
  const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
@@ -1594,7 +1636,7 @@ var clerkIntegration = {
1594
1636
  },
1595
1637
  {
1596
1638
  key: "clerk_secret_key",
1597
- description: "Clerk backend key (sk_*) \u2014 powers the odla->Clerk writes (account create, role + profile via public_metadata) and lets odla-db resolve user email/name via the Clerk API.",
1639
+ description: "Clerk backend key (sk_*) \u2014 powers account creation, the public role claim, and backend-only private profile signals; it also lets odla-db resolve user email/name via the Clerk API.",
1598
1640
  pattern: "sk_",
1599
1641
  mode: "full",
1600
1642
  vault: true
@@ -1614,8 +1656,13 @@ var clerkIntegration = {
1614
1656
  engine: "@odla-ai/chapter clerk.ts (Clerk Backend API via vault clerk_secret_key)",
1615
1657
  direction: "odla->provider",
1616
1658
  entity: "clerk user",
1617
- // Separate merge-PATCHes so a role write never clobbers a profile write.
1618
- fields: ["public_metadata.role", "public_metadata.profile"],
1659
+ // Separate metadata fields so a public role write cannot expose or
1660
+ // clobber backend-only account/profile signals.
1661
+ fields: [
1662
+ "public_metadata.role",
1663
+ "private_metadata.applicationId",
1664
+ "private_metadata.profile"
1665
+ ],
1619
1666
  onDelete: "n/a (writes only)"
1620
1667
  }
1621
1668
  ],
@@ -1992,6 +2039,8 @@ export {
1992
2039
  submitApplication,
1993
2040
  subscriptionIdempotencyKey,
1994
2041
  syncApplicationToCrm,
2042
+ updateClerkUserMetadata,
2043
+ updateClerkUserMetadataByEmail,
1995
2044
  validateScheduling,
1996
2045
  verifyStripeSignature,
1997
2046
  webhookMutationId