@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/README.md +40 -29
- package/dist/{chunk-OYXNI6VR.js → chunk-GMGTJIY3.js} +28 -15
- package/dist/chunk-GMGTJIY3.js.map +1 -0
- package/dist/{chunk-NTZSXAFT.js → chunk-WGQO4WDJ.js} +3 -2
- package/dist/chunk-WGQO4WDJ.js.map +1 -0
- package/dist/{chunk-VCC4S6JU.js → chunk-ZPAJL7KG.js} +28 -21
- package/dist/chunk-ZPAJL7KG.js.map +1 -0
- package/dist/{copy-context-pDogTR4x.d.ts → copy-context-DI21CYQ3.d.ts} +11 -5
- package/dist/index.cjs +70 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -25
- package/dist/index.d.ts +72 -25
- package/dist/index.js +70 -21
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +308 -108
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +11 -5
- package/dist/worker/index.d.ts +11 -5
- package/dist/worker/index.js +308 -108
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/adopt-existing.md +38 -9
- package/runbooks/greenfield.md +31 -31
- package/dist/chunk-NTZSXAFT.js.map +0 -1
- package/dist/chunk-OYXNI6VR.js.map +0 -1
- package/dist/chunk-VCC4S6JU.js.map +0 -1
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
|
-
|
|
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
|
|
324
|
-
* `
|
|
325
|
-
*
|
|
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
|
|
860
|
-
* `
|
|
861
|
-
* `focus` (clamped), but ONLY those in `application.profileFields
|
|
862
|
-
*
|
|
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
|
|
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
|
|
1088
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
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
|
|
1126
|
-
*
|
|
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
|
|
1147
|
-
*
|
|
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`,
|
|
1214
|
-
* `clerk.ts`) — and the `provision` split. Data only; it performs
|
|
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
|
-
|
|
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
|
|
324
|
-
* `
|
|
325
|
-
*
|
|
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
|
|
860
|
-
* `
|
|
861
|
-
* `focus` (clamped), but ONLY those in `application.profileFields
|
|
862
|
-
*
|
|
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
|
|
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
|
|
1088
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
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
|
|
1126
|
-
*
|
|
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
|
|
1147
|
-
*
|
|
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`,
|
|
1214
|
-
* `clerk.ts`) — and the `provision` split. Data only; it performs
|
|
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
|
|
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(
|
|
1375
|
+
await updateRecord2(recordDeps, { id: recordId, input });
|
|
1360
1376
|
} else {
|
|
1361
|
-
const created = await createRecord2(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1521
|
-
|
|
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 {
|
|
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
|
|
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
|
|
1618
|
-
|
|
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
|