@odla-ai/chapter 0.7.0 → 0.9.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
@@ -210,7 +210,14 @@ interface ChapterConfig {
210
210
  auth?: ChapterAuth;
211
211
  /** odla services (db implied). Default `["db","calendar","o11y"]`. */
212
212
  services?: readonly string[];
213
- }
213
+ /** Apply-time account provisioning: `"invite"` (default) mints a Clerk
214
+ * invitation, `"create"` makes the account server-side (so join can say the
215
+ * account is ready), `"none"` skips it. Any of these needs a `clerk_secret_key`
216
+ * vault secret to act. */
217
+ account?: AccountModel;
218
+ }
219
+ /** Apply-time Clerk account provisioning model. */
220
+ type AccountModel = "invite" | "create" | "none";
214
221
  /** The resolved engine `defineChapter()` returns. */
215
222
  interface Chapter {
216
223
  config: ChapterConfig;
@@ -230,6 +237,8 @@ interface Chapter {
230
237
  schema: DbSchema;
231
238
  rules: DbRules;
232
239
  services: readonly string[];
240
+ /** Resolved apply-time account provisioning model (default `"invite"`). */
241
+ account: AccountModel;
233
242
  /** The seed `groups` row derived from config (chapter mode), else `null`. */
234
243
  groupSeed(): Record<string, unknown> | null;
235
244
  }
@@ -692,11 +701,22 @@ declare function projectApplicant(deps: ProjectionDeps, applicant: Applicant): P
692
701
  recordId: string;
693
702
  }>;
694
703
 
704
+ /** The outcome of a Clerk provisioning call. `existed` marks the heal case: Clerk
705
+ * rejected it because the account/invitation is already there, which is the end
706
+ * state we wanted anyway. */
707
+ interface ClerkResult {
708
+ ok: boolean;
709
+ status: number;
710
+ existed?: boolean;
711
+ }
695
712
  /** Inputs for a Clerk invitation. */
696
713
  interface ClerkInviteInput {
697
714
  email: string;
698
715
  /** Where the accept-invitation link lands (usually the member area). */
699
716
  redirectUrl?: string;
717
+ /** Written to the invitation's `public_metadata`, so the accepted account
718
+ * carries your own profile fields. */
719
+ publicMetadata?: Record<string, unknown>;
700
720
  }
701
721
  /** Build the Clerk Backend API invitation request (path + JSON body). Pure, so
702
722
  * the wire shape is testable without a network call. */
@@ -704,13 +724,30 @@ declare function clerkInviteRequest(input: ClerkInviteInput): {
704
724
  path: string;
705
725
  body: Record<string, unknown>;
706
726
  };
707
- /** POST the invitation to the Clerk Backend API. `ignore_existing` isn't set, so
708
- * a repeat invite for an already-invited/known email returns non-ok the caller
709
- * swallows that (idempotent-enough for a best-effort apply-time invite). */
710
- declare function createClerkInvitation(secretKey: string, input: ClerkInviteInput, fetchImpl?: typeof fetch): Promise<{
711
- ok: boolean;
712
- status: number;
713
- }>;
727
+ /** POST the invitation to the Clerk Backend API. A repeat invite for an
728
+ * already-invited email heals to `{ ok: true, existed: true }` rather than
729
+ * reporting a failure the caller would have to special-case. */
730
+ declare function createClerkInvitation(secretKey: string, input: ClerkInviteInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
731
+ /** Inputs for a server-side Clerk user create. */
732
+ interface ClerkUserInput {
733
+ email: string;
734
+ firstName?: string;
735
+ lastName?: string;
736
+ /** Written to the user's `public_metadata` — the site's own profile fields
737
+ * (role, tier, whatever the member area reads). */
738
+ publicMetadata?: Record<string, unknown>;
739
+ }
740
+ /** Build the Clerk Backend API user-create request. The account is created
741
+ * passwordless (the member signs in via the site's Clerk flow), so join step 3
742
+ * can say the account is ready — the "create" alternative to an invitation. */
743
+ declare function clerkUserRequest(input: ClerkUserInput): {
744
+ path: string;
745
+ body: Record<string, unknown>;
746
+ };
747
+ /** Create the applicant's Clerk account server-side. A repeat for an email that
748
+ * already has an account heals to `{ ok: true, existed: true }` — the account
749
+ * exists, which is the state apply-time provisioning wanted. */
750
+ declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
714
751
 
715
752
  /** An application row, as far as the session cares about it. */
716
753
  interface ApplicationRecord {
@@ -934,4 +971,4 @@ type ApplicationBookingPatch = {
934
971
  * already there (never backward). */
935
972
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
936
973
 
937
- export { 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 ClerkInviteInput, 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 RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clerkInviteRequest, createChapterIntegration, createClerkInvitation, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, 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, verifyStripeSignature, webhookMutationId };
974
+ export { type AccountModel, 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 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 RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clerkInviteRequest, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, 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, verifyStripeSignature, webhookMutationId };
package/dist/index.d.ts CHANGED
@@ -210,7 +210,14 @@ interface ChapterConfig {
210
210
  auth?: ChapterAuth;
211
211
  /** odla services (db implied). Default `["db","calendar","o11y"]`. */
212
212
  services?: readonly string[];
213
- }
213
+ /** Apply-time account provisioning: `"invite"` (default) mints a Clerk
214
+ * invitation, `"create"` makes the account server-side (so join can say the
215
+ * account is ready), `"none"` skips it. Any of these needs a `clerk_secret_key`
216
+ * vault secret to act. */
217
+ account?: AccountModel;
218
+ }
219
+ /** Apply-time Clerk account provisioning model. */
220
+ type AccountModel = "invite" | "create" | "none";
214
221
  /** The resolved engine `defineChapter()` returns. */
215
222
  interface Chapter {
216
223
  config: ChapterConfig;
@@ -230,6 +237,8 @@ interface Chapter {
230
237
  schema: DbSchema;
231
238
  rules: DbRules;
232
239
  services: readonly string[];
240
+ /** Resolved apply-time account provisioning model (default `"invite"`). */
241
+ account: AccountModel;
233
242
  /** The seed `groups` row derived from config (chapter mode), else `null`. */
234
243
  groupSeed(): Record<string, unknown> | null;
235
244
  }
@@ -692,11 +701,22 @@ declare function projectApplicant(deps: ProjectionDeps, applicant: Applicant): P
692
701
  recordId: string;
693
702
  }>;
694
703
 
704
+ /** The outcome of a Clerk provisioning call. `existed` marks the heal case: Clerk
705
+ * rejected it because the account/invitation is already there, which is the end
706
+ * state we wanted anyway. */
707
+ interface ClerkResult {
708
+ ok: boolean;
709
+ status: number;
710
+ existed?: boolean;
711
+ }
695
712
  /** Inputs for a Clerk invitation. */
696
713
  interface ClerkInviteInput {
697
714
  email: string;
698
715
  /** Where the accept-invitation link lands (usually the member area). */
699
716
  redirectUrl?: string;
717
+ /** Written to the invitation's `public_metadata`, so the accepted account
718
+ * carries your own profile fields. */
719
+ publicMetadata?: Record<string, unknown>;
700
720
  }
701
721
  /** Build the Clerk Backend API invitation request (path + JSON body). Pure, so
702
722
  * the wire shape is testable without a network call. */
@@ -704,13 +724,30 @@ declare function clerkInviteRequest(input: ClerkInviteInput): {
704
724
  path: string;
705
725
  body: Record<string, unknown>;
706
726
  };
707
- /** POST the invitation to the Clerk Backend API. `ignore_existing` isn't set, so
708
- * a repeat invite for an already-invited/known email returns non-ok the caller
709
- * swallows that (idempotent-enough for a best-effort apply-time invite). */
710
- declare function createClerkInvitation(secretKey: string, input: ClerkInviteInput, fetchImpl?: typeof fetch): Promise<{
711
- ok: boolean;
712
- status: number;
713
- }>;
727
+ /** POST the invitation to the Clerk Backend API. A repeat invite for an
728
+ * already-invited email heals to `{ ok: true, existed: true }` rather than
729
+ * reporting a failure the caller would have to special-case. */
730
+ declare function createClerkInvitation(secretKey: string, input: ClerkInviteInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
731
+ /** Inputs for a server-side Clerk user create. */
732
+ interface ClerkUserInput {
733
+ email: string;
734
+ firstName?: string;
735
+ lastName?: string;
736
+ /** Written to the user's `public_metadata` — the site's own profile fields
737
+ * (role, tier, whatever the member area reads). */
738
+ publicMetadata?: Record<string, unknown>;
739
+ }
740
+ /** Build the Clerk Backend API user-create request. The account is created
741
+ * passwordless (the member signs in via the site's Clerk flow), so join step 3
742
+ * can say the account is ready — the "create" alternative to an invitation. */
743
+ declare function clerkUserRequest(input: ClerkUserInput): {
744
+ path: string;
745
+ body: Record<string, unknown>;
746
+ };
747
+ /** Create the applicant's Clerk account server-side. A repeat for an email that
748
+ * already has an account heals to `{ ok: true, existed: true }` — the account
749
+ * exists, which is the state apply-time provisioning wanted. */
750
+ declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
714
751
 
715
752
  /** An application row, as far as the session cares about it. */
716
753
  interface ApplicationRecord {
@@ -934,4 +971,4 @@ type ApplicationBookingPatch = {
934
971
  * already there (never backward). */
935
972
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
936
973
 
937
- export { 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 ClerkInviteInput, 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 RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clerkInviteRequest, createChapterIntegration, createClerkInvitation, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, 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, verifyStripeSignature, webhookMutationId };
974
+ export { type AccountModel, 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 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 RoleChangeContext, type Rule, SCHEDULING_DEFAULTS, type SecretStore, type SessionUser, type SharedPerson, type StripeEvent, type SubmitResult, type WebhookEvent, applicationBookingUpdate, applicationSummary, bookingDecision, brandTokens, buildGroupSeed, canApprove, canBook, canChangeRole, canTransition, canceledPatch, chapterDb, clerkInviteRequest, clerkUserRequest, createChapterIntegration, createClerkInvitation, createClerkUser, defaultCrm, defineChapter, emailGroupFrom, endForSlot, findApplicationRef, firstPaymentPatch, getVaultSecret, introIdempotencyKey, isAdminRole, isAlreadySent, isReconcilable, isSlotAvailable, 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, verifyStripeSignature, webhookMutationId };
package/dist/index.js CHANGED
@@ -226,7 +226,7 @@ Warmly,
226
226
  ${name}`;
227
227
  return {
228
228
  adminNotification: {
229
- subject: `New application \u2014 {{firstName}} {{lastName}}`,
229
+ subject: `New application: {{firstName}} {{lastName}}`,
230
230
  text: `A new application came in for ${name}.
231
231
 
232
232
  Name: {{firstName}} {{lastName}}
@@ -245,7 +245,7 @@ Your membership payment is confirmed. We'll be in touch to schedule your intro c
245
245
  Looking forward to our call at {{meetingTime}}. {{meetingLink}}${sign}`
246
246
  },
247
247
  onboardingInvite: {
248
- subject: `You're in \u2014 ${name}`,
248
+ subject: `You're in, ${name}`,
249
249
  text: `Hi {{firstName}},
250
250
 
251
251
  Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
@@ -470,6 +470,10 @@ function defineChapter(config) {
470
470
  const application = resolveApplication(config.application);
471
471
  const { schema, rules } = chapterDb(mode, auth);
472
472
  const services = config.services ?? ["db", "calendar", "o11y"];
473
+ const account = config.account ?? "invite";
474
+ if (account !== "invite" && account !== "create" && account !== "none") {
475
+ throw new Error(`defineChapter.account: must be "invite", "create", or "none" \u2014 got "${String(account)}"`);
476
+ }
473
477
  const chapter = {
474
478
  config,
475
479
  id: id2,
@@ -482,6 +486,7 @@ function defineChapter(config) {
482
486
  schema,
483
487
  rules,
484
488
  services,
489
+ account,
485
490
  groupSeed: () => mode === "chapter" ? buildGroupSeed(config) : null
486
491
  };
487
492
  if (config.url !== void 0) chapter.url = config.url;
@@ -508,7 +513,7 @@ function createChapterIntegration(chapter, options = {}) {
508
513
  }
509
514
  return {
510
515
  id: "chapter",
511
- title: `Chapter \u2014 ${chapter.name}`,
516
+ title: `Chapter: ${chapter.name}`,
512
517
  npm: "@odla-ai/chapter",
513
518
  schema: {
514
519
  entities: { ...crmDesc.schema.entities, ...chapter.schema.entities },
@@ -759,13 +764,15 @@ async function projectApplicant(deps, applicant) {
759
764
  }
760
765
 
761
766
  // src/clerk.ts
767
+ var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
762
768
  function clerkInviteRequest(input) {
763
769
  return {
764
770
  path: "/v1/invitations",
765
771
  body: {
766
772
  email_address: input.email,
767
773
  notify: true,
768
- ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {}
774
+ ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
775
+ ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
769
776
  }
770
777
  };
771
778
  }
@@ -776,7 +783,28 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
776
783
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
777
784
  body: JSON.stringify(body)
778
785
  });
779
- return { ok: res.ok, status: res.status };
786
+ return res.ok ? { ok: true, status: res.status } : heal(res.status);
787
+ }
788
+ function clerkUserRequest(input) {
789
+ return {
790
+ path: "/v1/users",
791
+ body: {
792
+ email_address: [input.email],
793
+ skip_password_requirement: true,
794
+ ...input.firstName ? { first_name: input.firstName } : {},
795
+ ...input.lastName ? { last_name: input.lastName } : {},
796
+ ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
797
+ }
798
+ };
799
+ }
800
+ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
801
+ const { path, body } = clerkUserRequest(input);
802
+ const res = await fetchImpl(`https://api.clerk.com${path}`, {
803
+ method: "POST",
804
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
805
+ body: JSON.stringify(body)
806
+ });
807
+ return res.ok ? { ok: true, status: res.status } : heal(res.status);
780
808
  }
781
809
 
782
810
  // src/session.ts
@@ -980,8 +1008,10 @@ export {
980
1008
  canceledPatch,
981
1009
  chapterDb,
982
1010
  clerkInviteRequest,
1011
+ clerkUserRequest,
983
1012
  createChapterIntegration,
984
1013
  createClerkInvitation,
1014
+ createClerkUser,
985
1015
  defaultCrm,
986
1016
  defineChapter,
987
1017
  emailGroupFrom,