@odla-ai/chapter 0.12.0 → 0.14.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
@@ -833,6 +833,100 @@ declare function clerkUserRequest(input: ClerkUserInput): {
833
833
  * re-application repairs a previously missed create (`refreshed: true`). */
834
834
  declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
835
835
 
836
+ /** A Clerk user as chapter's role layer sees it. `role` defaults to the lowest
837
+ * rung when `public_metadata` carries none; `publicMetadata` is returned raw so a
838
+ * site with a custom ladder can re-derive it. */
839
+ interface ClerkUserRecord {
840
+ id: string;
841
+ email?: string;
842
+ role: string;
843
+ publicMetadata: Record<string, unknown>;
844
+ }
845
+ /** Look a Clerk user up by email. `null` when no such user — or when the lookup
846
+ * fails (a role gate treats an unresolvable user as absent, matching the site's
847
+ * own fallback). Role defaults to provisional when unset. */
848
+ declare function clerkGetUserByEmail(secretKey: string, email: string, fetchImpl?: typeof fetch): Promise<ClerkUserRecord | null>;
849
+ /** Fetch a Clerk user by id, for role-change gating. `null` when missing or on a
850
+ * failed lookup. Role defaults to provisional when unset. */
851
+ declare function clerkGetUser(secretKey: string, id: string, fetchImpl?: typeof fetch): Promise<ClerkUserRecord | null>;
852
+ /** List ALL Clerk users with their roles, auto-paginating. A membership community
853
+ * outgrows one page, and a fixed `limit=100` would silently drop members from the
854
+ * admin roster with no error — so this pages (offset in steps of {@link PAGE})
855
+ * until a short page. A page fetch that fails THROWS rather than returning a
856
+ * partial list, so the caller never mistakes a truncated roster for the whole. */
857
+ declare function clerkListUsers(secretKey: string, fetchImpl?: typeof fetch): Promise<ClerkUserRecord[]>;
858
+ /** Write a user's role: a MERGE-PATCH of only `{ role }` on `public_metadata`, so
859
+ * it leaves a separately-written `profile` untouched. Returns whether it stuck. */
860
+ declare function clerkSetRole(secretKey: string, id: string, role: string, fetchImpl?: typeof fetch): Promise<boolean>;
861
+
862
+ /** Which auth mode a secret is required for: `"client"` (publishable key only) or
863
+ * `"full"` (server-side profile/role reads + writes, needing the vault secret). */
864
+ type SecretMode = "client" | "full";
865
+ /** A configuration value the capability needs (e.g. the publishable key). */
866
+ interface IntegrationSetting {
867
+ key: string;
868
+ description: string;
869
+ /** Safe to ship to the browser? (publishable keys are; secrets are not). */
870
+ public: boolean;
871
+ /** Expected value shape, e.g. a "pk_" prefix. */
872
+ pattern?: string;
873
+ /** Stored per environment (dev/prod)? */
874
+ perEnv: boolean;
875
+ /** Where the value is stored and read from. */
876
+ source: string;
877
+ }
878
+ /** A secret the capability needs, held in the tenant vault. */
879
+ interface IntegrationSecret {
880
+ key: string;
881
+ description: string;
882
+ pattern?: string;
883
+ /** Auth mode that requires this secret ("full" pulls user profiles). */
884
+ mode: SecretMode;
885
+ /** Stored in the tenant vault (never in graph rows or wrangler vars)? */
886
+ vault: boolean;
887
+ }
888
+ /** One data-sync contract. A capability may declare several (e.g. an inbound
889
+ * webhook mirror AND an outbound write), so {@link IntegrationDescriptor.syncs}
890
+ * is a list. The mechanism is declared, not reimplemented here. */
891
+ interface IntegrationSync {
892
+ engine: string;
893
+ direction: "provider->odla" | "odla->provider" | "bidirectional";
894
+ entity: string;
895
+ fields: string[];
896
+ webhook?: string;
897
+ onDelete?: string;
898
+ }
899
+ /** The provisioning split: human, CLI, and doctor-assertable steps. */
900
+ interface IntegrationProvision {
901
+ /** Steps a human performs (out-of-band, e.g. in the Clerk dashboard). */
902
+ human: string[];
903
+ /** Steps the odla CLI automates. */
904
+ cli: string[];
905
+ /** Checks `odla doctor` should assert. */
906
+ doctor: string[];
907
+ }
908
+ /** Documentation-as-data for a provider capability, consumed by the odla CLI,
909
+ * `odla doctor`, and the docs. */
910
+ interface IntegrationDescriptor {
911
+ id: string;
912
+ title: string;
913
+ npm: string;
914
+ settings: IntegrationSetting[];
915
+ secrets: IntegrationSecret[];
916
+ /** Every data-sync contract the capability declares, each with its direction. */
917
+ syncs: IntegrationSync[];
918
+ provision: IntegrationProvision;
919
+ }
920
+ /**
921
+ * The concrete {@link IntegrationDescriptor} for the Clerk capability. Declares
922
+ * the one public `setting` (the `pk_*` publishable key, served to the SPA), the
923
+ * `secrets` auth mode `"full"` needs in the tenant vault, BOTH sync directions —
924
+ * the inbound `$users` webhook mirror (`provider->odla`) and the outbound
925
+ * role/profile write via `clerk_secret_key` (`odla->provider`, chapter's
926
+ * `clerk.ts`) — and the `provision` split. Data only; it performs none of it.
927
+ */
928
+ declare const clerkIntegration: IntegrationDescriptor;
929
+
836
930
  /** An application row, as far as the session cares about it. */
837
931
  interface ApplicationRecord {
838
932
  id: string;
@@ -1071,4 +1165,4 @@ type ApplicationBookingPatch = {
1071
1165
  * already there (never backward). */
1072
1166
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
1073
1167
 
1074
- 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 ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, 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 ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clampArray, clerkInviteRequest, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, slotWindow, stageIndex, stripeForm, submitApplication, subscriptionIdempotencyKey, validateScheduling, verifyStripeSignature, webhookMutationId };
1168
+ 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 ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, 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 ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, slotWindow, stageIndex, stripeForm, submitApplication, subscriptionIdempotencyKey, validateScheduling, verifyStripeSignature, webhookMutationId };
package/dist/index.d.ts CHANGED
@@ -833,6 +833,100 @@ declare function clerkUserRequest(input: ClerkUserInput): {
833
833
  * re-application repairs a previously missed create (`refreshed: true`). */
834
834
  declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
835
835
 
836
+ /** A Clerk user as chapter's role layer sees it. `role` defaults to the lowest
837
+ * rung when `public_metadata` carries none; `publicMetadata` is returned raw so a
838
+ * site with a custom ladder can re-derive it. */
839
+ interface ClerkUserRecord {
840
+ id: string;
841
+ email?: string;
842
+ role: string;
843
+ publicMetadata: Record<string, unknown>;
844
+ }
845
+ /** Look a Clerk user up by email. `null` when no such user — or when the lookup
846
+ * fails (a role gate treats an unresolvable user as absent, matching the site's
847
+ * own fallback). Role defaults to provisional when unset. */
848
+ declare function clerkGetUserByEmail(secretKey: string, email: string, fetchImpl?: typeof fetch): Promise<ClerkUserRecord | null>;
849
+ /** Fetch a Clerk user by id, for role-change gating. `null` when missing or on a
850
+ * failed lookup. Role defaults to provisional when unset. */
851
+ declare function clerkGetUser(secretKey: string, id: string, fetchImpl?: typeof fetch): Promise<ClerkUserRecord | null>;
852
+ /** List ALL Clerk users with their roles, auto-paginating. A membership community
853
+ * outgrows one page, and a fixed `limit=100` would silently drop members from the
854
+ * admin roster with no error — so this pages (offset in steps of {@link PAGE})
855
+ * until a short page. A page fetch that fails THROWS rather than returning a
856
+ * partial list, so the caller never mistakes a truncated roster for the whole. */
857
+ declare function clerkListUsers(secretKey: string, fetchImpl?: typeof fetch): Promise<ClerkUserRecord[]>;
858
+ /** Write a user's role: a MERGE-PATCH of only `{ role }` on `public_metadata`, so
859
+ * it leaves a separately-written `profile` untouched. Returns whether it stuck. */
860
+ declare function clerkSetRole(secretKey: string, id: string, role: string, fetchImpl?: typeof fetch): Promise<boolean>;
861
+
862
+ /** Which auth mode a secret is required for: `"client"` (publishable key only) or
863
+ * `"full"` (server-side profile/role reads + writes, needing the vault secret). */
864
+ type SecretMode = "client" | "full";
865
+ /** A configuration value the capability needs (e.g. the publishable key). */
866
+ interface IntegrationSetting {
867
+ key: string;
868
+ description: string;
869
+ /** Safe to ship to the browser? (publishable keys are; secrets are not). */
870
+ public: boolean;
871
+ /** Expected value shape, e.g. a "pk_" prefix. */
872
+ pattern?: string;
873
+ /** Stored per environment (dev/prod)? */
874
+ perEnv: boolean;
875
+ /** Where the value is stored and read from. */
876
+ source: string;
877
+ }
878
+ /** A secret the capability needs, held in the tenant vault. */
879
+ interface IntegrationSecret {
880
+ key: string;
881
+ description: string;
882
+ pattern?: string;
883
+ /** Auth mode that requires this secret ("full" pulls user profiles). */
884
+ mode: SecretMode;
885
+ /** Stored in the tenant vault (never in graph rows or wrangler vars)? */
886
+ vault: boolean;
887
+ }
888
+ /** One data-sync contract. A capability may declare several (e.g. an inbound
889
+ * webhook mirror AND an outbound write), so {@link IntegrationDescriptor.syncs}
890
+ * is a list. The mechanism is declared, not reimplemented here. */
891
+ interface IntegrationSync {
892
+ engine: string;
893
+ direction: "provider->odla" | "odla->provider" | "bidirectional";
894
+ entity: string;
895
+ fields: string[];
896
+ webhook?: string;
897
+ onDelete?: string;
898
+ }
899
+ /** The provisioning split: human, CLI, and doctor-assertable steps. */
900
+ interface IntegrationProvision {
901
+ /** Steps a human performs (out-of-band, e.g. in the Clerk dashboard). */
902
+ human: string[];
903
+ /** Steps the odla CLI automates. */
904
+ cli: string[];
905
+ /** Checks `odla doctor` should assert. */
906
+ doctor: string[];
907
+ }
908
+ /** Documentation-as-data for a provider capability, consumed by the odla CLI,
909
+ * `odla doctor`, and the docs. */
910
+ interface IntegrationDescriptor {
911
+ id: string;
912
+ title: string;
913
+ npm: string;
914
+ settings: IntegrationSetting[];
915
+ secrets: IntegrationSecret[];
916
+ /** Every data-sync contract the capability declares, each with its direction. */
917
+ syncs: IntegrationSync[];
918
+ provision: IntegrationProvision;
919
+ }
920
+ /**
921
+ * The concrete {@link IntegrationDescriptor} for the Clerk capability. Declares
922
+ * the one public `setting` (the `pk_*` publishable key, served to the SPA), the
923
+ * `secrets` auth mode `"full"` needs in the tenant vault, BOTH sync directions —
924
+ * the inbound `$users` webhook mirror (`provider->odla`) and the outbound
925
+ * role/profile write via `clerk_secret_key` (`odla->provider`, chapter's
926
+ * `clerk.ts`) — and the `provision` split. Data only; it performs none of it.
927
+ */
928
+ declare const clerkIntegration: IntegrationDescriptor;
929
+
836
930
  /** An application row, as far as the session cares about it. */
837
931
  interface ApplicationRecord {
838
932
  id: string;
@@ -1071,4 +1165,4 @@ type ApplicationBookingPatch = {
1071
1165
  * already there (never backward). */
1072
1166
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
1073
1167
 
1074
- 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 ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type DbOp, type DbRules, type DbSchema, type DeliveryDecision, type EmailGroup, type EmailTemplate, type EmailTemplateRow, type Entity, type ExistingMeeting, type GuardResult, 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 ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clampArray, clerkInviteRequest, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, slotWindow, stageIndex, stripeForm, submitApplication, subscriptionIdempotencyKey, validateScheduling, verifyStripeSignature, webhookMutationId };
1168
+ 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 ChapterDb, type ChapterEmails, type ChapterIntegrationDescriptor, type ChapterIntegrationOptions, type ChapterMode, type ChapterPipeline, type ChapterPolicy, type ChapterPrices, type ChapterScheduling, type ChapterSends, type ClerkInviteInput, type ClerkResult, type ClerkUserInput, type ClerkUserRecord, 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 ResolvedPipeline, type ResolvedScheduling, type ResolvedSends, type RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SchedulingErrors, type SecretMode, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicantProfile, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clampArray, clerkGetUser, clerkGetUserByEmail, clerkIntegration, clerkInviteRequest, clerkListUsers, clerkSetRole, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, hasDisclaimerAck, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, isValidEmail, joinConfig, meetingCreateRow, meetingRescheduleUpdate, memberApplication, memberSession, normalizeWebhookEvent, paymentsReady, planDelivery, projectApplicant, projectSharedRecord, reconcileMeetings, refundedPatch, render, renderSummary, renderTemplateBody, renewalPatch, resolveApplication, resolveAuth, resolvePipeline, resolveScheduling, roleFromClaim, sendTemplated, sharedPersonInput, slotWindow, stageIndex, stripeForm, submitApplication, subscriptionIdempotencyKey, validateScheduling, verifyStripeSignature, webhookMutationId };
package/dist/index.js CHANGED
@@ -883,6 +883,119 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
883
883
  return { ...healed, refreshed };
884
884
  }
885
885
 
886
+ // src/clerk-roles.ts
887
+ var CLERK_API = "https://api.clerk.com";
888
+ var DEFAULT_ROLE = "provisional";
889
+ var PAGE = 100;
890
+ function toRecord(u) {
891
+ if (typeof u.id !== "string") return null;
892
+ const pm = u.public_metadata ?? {};
893
+ const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
894
+ const email = u.email_addresses?.[0]?.email_address;
895
+ return { id: u.id, email: typeof email === "string" ? email : void 0, role, publicMetadata: pm };
896
+ }
897
+ async function clerkGet(path, secretKey, fetchImpl) {
898
+ const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
899
+ if (!res.ok) throw new Error(`clerk GET ${path} \u2192 ${res.status}`);
900
+ return res.json();
901
+ }
902
+ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
903
+ const data = await clerkGet(`/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, secretKey, fetchImpl).catch(() => null);
904
+ const user = Array.isArray(data) ? data[0] : void 0;
905
+ return user ? toRecord(user) : null;
906
+ }
907
+ async function clerkGetUser(secretKey, id2, fetchImpl = fetch) {
908
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id2)}`, secretKey, fetchImpl).catch(() => null);
909
+ return data ? toRecord(data) : null;
910
+ }
911
+ async function clerkListUsers(secretKey, fetchImpl = fetch) {
912
+ const out = [];
913
+ for (let offset = 0; ; offset += PAGE) {
914
+ const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
915
+ const page = Array.isArray(data) ? data : [];
916
+ for (const u of page) {
917
+ const record = toRecord(u);
918
+ if (record) out.push(record);
919
+ }
920
+ if (page.length < PAGE) break;
921
+ }
922
+ return out;
923
+ }
924
+ async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
925
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id2)}/metadata`, {
926
+ method: "PATCH",
927
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
928
+ body: JSON.stringify({ public_metadata: { role } })
929
+ });
930
+ return res.ok;
931
+ }
932
+
933
+ // src/clerk-integration.ts
934
+ var clerkIntegration = {
935
+ id: "clerk",
936
+ title: "Clerk end-user auth",
937
+ npm: "@odla-ai/chapter",
938
+ settings: [
939
+ {
940
+ key: "publishableKey",
941
+ description: "Clerk publishable key (pk_*). Public by design; served to the SPA (loads clerk-js from Clerk's CDN via @odla-ai/auth-clerk).",
942
+ public: true,
943
+ pattern: "pk_",
944
+ perEnv: true,
945
+ source: "apps-registry auth[env].publishableKey (from odla.config.mjs auth.clerk.<env>)"
946
+ }
947
+ ],
948
+ secrets: [
949
+ {
950
+ key: "CLERK_WEBHOOK_SECRET",
951
+ description: "Svix signing secret for Clerk user webhooks \u2014 verifies $users sync events.",
952
+ pattern: "whsec_",
953
+ mode: "full",
954
+ vault: true
955
+ },
956
+ {
957
+ key: "clerk_secret_key",
958
+ 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.",
959
+ pattern: "sk_",
960
+ mode: "full",
961
+ vault: true
962
+ }
963
+ ],
964
+ syncs: [
965
+ {
966
+ engine: "@odla-ai/db $users (Clerk webhook -> tenant graph)",
967
+ direction: "provider->odla",
968
+ entity: "$users",
969
+ // Matches the webhook mapper: the mirror carries id + primary email + name + avatar.
970
+ fields: ["id", "email", "name", "imageUrl"],
971
+ webhook: "whsec_ (svix-signed)",
972
+ onDelete: "tombstone (never row-delete)"
973
+ },
974
+ {
975
+ engine: "@odla-ai/chapter clerk.ts (Clerk Backend API via vault clerk_secret_key)",
976
+ direction: "odla->provider",
977
+ entity: "clerk user",
978
+ // Separate merge-PATCHes so a role write never clobbers a profile write.
979
+ fields: ["public_metadata.role", "public_metadata.profile"],
980
+ onDelete: "n/a (writes only)"
981
+ }
982
+ ],
983
+ provision: {
984
+ human: [
985
+ "Run `npx clerk auth login` \u2014 the one interactive step; the agent then creates + configures the Clerk app with the Clerk CLI (no pk_ to hand-paste).",
986
+ 'For auth mode "full": create the Clerk user-sync webhook and store its whsec_, plus the sk_ as clerk_secret_key, in the tenant vault (Studio, write-only).'
987
+ ],
988
+ cli: [
989
+ "Clerk CLI (`npx clerk apps create/link/config patch`) creates + configures the instance and pulls the pk_.",
990
+ "odla `provision` records it: setAuth(env, publishableKey) -> apps-registry (issuer/JWKS derived from the key)."
991
+ ],
992
+ doctor: [
993
+ "registry auth[env] present when the app mounts <SignIn>?",
994
+ 'auth mode "full" => whsec_ and clerk_secret_key present in the tenant vault?'
995
+ ]
996
+ }
997
+ };
998
+
886
999
  // src/session.ts
887
1000
  function applicationSummary(app) {
888
1001
  return {
@@ -1100,7 +1213,12 @@ export {
1100
1213
  canceledPatch,
1101
1214
  chapterDb,
1102
1215
  clampArray,
1216
+ clerkGetUser,
1217
+ clerkGetUserByEmail,
1218
+ clerkIntegration,
1103
1219
  clerkInviteRequest,
1220
+ clerkListUsers,
1221
+ clerkSetRole,
1104
1222
  clerkUserRequest,
1105
1223
  createChapterIntegration,
1106
1224
  createClerkInvitation,