@odla-ai/chapter 0.9.0 → 0.10.1

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
@@ -215,9 +215,24 @@ interface ChapterConfig {
215
215
  * account is ready), `"none"` skips it. Any of these needs a `clerk_secret_key`
216
216
  * vault secret to act. */
217
217
  account?: AccountModel;
218
+ /** WHEN lifecycle email fires. Addressing and content live on the group row
219
+ * (owner-editable at runtime); this is the trigger, which is a build-time
220
+ * decision. See {@link ChapterSends}. */
221
+ sends?: ChapterSends;
218
222
  }
219
223
  /** Apply-time Clerk account provisioning model. */
220
224
  type AccountModel = "invite" | "create" | "none";
225
+ /** When the admin notification fires: on application `submit` (default), on the
226
+ * first successful `payment`, or `never` (the site drives it itself). */
227
+ type AdminNotificationTrigger = "submit" | "payment" | "never";
228
+ /** Send-policy config: the trigger for each lifecycle email chapter owns. */
229
+ interface ChapterSends {
230
+ adminNotification?: AdminNotificationTrigger;
231
+ }
232
+ /** Resolved send policy (every trigger present). */
233
+ interface ResolvedSends {
234
+ adminNotification: AdminNotificationTrigger;
235
+ }
221
236
  /** The resolved engine `defineChapter()` returns. */
222
237
  interface Chapter {
223
238
  config: ChapterConfig;
@@ -239,6 +254,8 @@ interface Chapter {
239
254
  services: readonly string[];
240
255
  /** Resolved apply-time account provisioning model (default `"invite"`). */
241
256
  account: AccountModel;
257
+ /** Resolved send policy — when each lifecycle email fires. */
258
+ sends: ResolvedSends;
242
259
  /** The seed `groups` row derived from config (chapter mode), else `null`. */
243
260
  groupSeed(): Record<string, unknown> | null;
244
261
  }
@@ -608,6 +625,14 @@ declare function canceledPatch(): {
608
625
 
609
626
  /** Apply defaults + validate the application config. Throws at import on bad shape. */
610
627
  declare function resolveApplication(a: ChapterApplication | undefined): ResolvedApplication;
628
+ /**
629
+ * The applicant profile written to the Clerk account's `public_metadata.profile`:
630
+ * every configured application field Clerk does not already carry natively, plus
631
+ * `focus`. Derived from `application.required`/`optional`, so a site's own field
632
+ * names project without this package knowing them. Pure; returns `undefined` when
633
+ * there is nothing to write.
634
+ */
635
+ declare function applicantProfile(chapter: Chapter, fields: Record<string, unknown>): Record<string, unknown> | undefined;
611
636
  /** A validated submission, or a 400-worthy validation error the route returns. */
612
637
  type SubmitResult = {
613
638
  ok: true;
@@ -708,6 +733,8 @@ interface ClerkResult {
708
733
  ok: boolean;
709
734
  status: number;
710
735
  existed?: boolean;
736
+ /** Set when an `existed` heal also refreshed the account's public_metadata. */
737
+ refreshed?: boolean;
711
738
  }
712
739
  /** Inputs for a Clerk invitation. */
713
740
  interface ClerkInviteInput {
@@ -746,7 +773,9 @@ declare function clerkUserRequest(input: ClerkUserInput): {
746
773
  };
747
774
  /** Create the applicant's Clerk account server-side. A repeat for an email that
748
775
  * already has an account heals to `{ ok: true, existed: true }` — the account
749
- * exists, which is the state apply-time provisioning wanted. */
776
+ * exists, which is the state apply-time provisioning wanted — and, when
777
+ * `publicMetadata` was supplied, refreshes it on the existing account so a
778
+ * re-application repairs a previously missed create (`refreshed: true`). */
750
779
  declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
751
780
 
752
781
  /** An application row, as far as the session cares about it. */
@@ -888,6 +917,22 @@ declare const SCHEDULING_DEFAULTS: ResolvedScheduling;
888
917
  * `windowDays` is capped at 62 because Google FreeBusy is.
889
918
  */
890
919
  declare function resolveScheduling(config?: ChapterScheduling): ResolvedScheduling;
920
+ /** Validation messages keyed by form field. */
921
+ type SchedulingErrors = Record<string, string>;
922
+ /**
923
+ * Validate a scheduling config field by field, collecting owner-readable messages
924
+ * rather than throwing on the first problem. An admin settings form needs to say
925
+ * *which* field is wrong and why — "pick at least one day" beats a stack trace —
926
+ * so the admin route returns these directly. {@link resolveScheduling} is the
927
+ * throwing wrapper for internal/config-time use.
928
+ */
929
+ declare function validateScheduling(config?: ChapterScheduling): {
930
+ ok: true;
931
+ value: ResolvedScheduling;
932
+ } | {
933
+ ok: false;
934
+ errors: SchedulingErrors;
935
+ };
891
936
  /** Statuses a member may book/reschedule from (early pipeline only). */
892
937
  /** The availability window: `[now, now + windowDays]` in epoch ms. */
893
938
  declare function slotWindow(now: number, windowDays: number): {
@@ -971,4 +1016,4 @@ type ApplicationBookingPatch = {
971
1016
  * already there (never backward). */
972
1017
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
973
1018
 
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 };
1019
+ 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, 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, validateScheduling, verifyStripeSignature, webhookMutationId };
package/dist/index.d.ts CHANGED
@@ -215,9 +215,24 @@ interface ChapterConfig {
215
215
  * account is ready), `"none"` skips it. Any of these needs a `clerk_secret_key`
216
216
  * vault secret to act. */
217
217
  account?: AccountModel;
218
+ /** WHEN lifecycle email fires. Addressing and content live on the group row
219
+ * (owner-editable at runtime); this is the trigger, which is a build-time
220
+ * decision. See {@link ChapterSends}. */
221
+ sends?: ChapterSends;
218
222
  }
219
223
  /** Apply-time Clerk account provisioning model. */
220
224
  type AccountModel = "invite" | "create" | "none";
225
+ /** When the admin notification fires: on application `submit` (default), on the
226
+ * first successful `payment`, or `never` (the site drives it itself). */
227
+ type AdminNotificationTrigger = "submit" | "payment" | "never";
228
+ /** Send-policy config: the trigger for each lifecycle email chapter owns. */
229
+ interface ChapterSends {
230
+ adminNotification?: AdminNotificationTrigger;
231
+ }
232
+ /** Resolved send policy (every trigger present). */
233
+ interface ResolvedSends {
234
+ adminNotification: AdminNotificationTrigger;
235
+ }
221
236
  /** The resolved engine `defineChapter()` returns. */
222
237
  interface Chapter {
223
238
  config: ChapterConfig;
@@ -239,6 +254,8 @@ interface Chapter {
239
254
  services: readonly string[];
240
255
  /** Resolved apply-time account provisioning model (default `"invite"`). */
241
256
  account: AccountModel;
257
+ /** Resolved send policy — when each lifecycle email fires. */
258
+ sends: ResolvedSends;
242
259
  /** The seed `groups` row derived from config (chapter mode), else `null`. */
243
260
  groupSeed(): Record<string, unknown> | null;
244
261
  }
@@ -608,6 +625,14 @@ declare function canceledPatch(): {
608
625
 
609
626
  /** Apply defaults + validate the application config. Throws at import on bad shape. */
610
627
  declare function resolveApplication(a: ChapterApplication | undefined): ResolvedApplication;
628
+ /**
629
+ * The applicant profile written to the Clerk account's `public_metadata.profile`:
630
+ * every configured application field Clerk does not already carry natively, plus
631
+ * `focus`. Derived from `application.required`/`optional`, so a site's own field
632
+ * names project without this package knowing them. Pure; returns `undefined` when
633
+ * there is nothing to write.
634
+ */
635
+ declare function applicantProfile(chapter: Chapter, fields: Record<string, unknown>): Record<string, unknown> | undefined;
611
636
  /** A validated submission, or a 400-worthy validation error the route returns. */
612
637
  type SubmitResult = {
613
638
  ok: true;
@@ -708,6 +733,8 @@ interface ClerkResult {
708
733
  ok: boolean;
709
734
  status: number;
710
735
  existed?: boolean;
736
+ /** Set when an `existed` heal also refreshed the account's public_metadata. */
737
+ refreshed?: boolean;
711
738
  }
712
739
  /** Inputs for a Clerk invitation. */
713
740
  interface ClerkInviteInput {
@@ -746,7 +773,9 @@ declare function clerkUserRequest(input: ClerkUserInput): {
746
773
  };
747
774
  /** Create the applicant's Clerk account server-side. A repeat for an email that
748
775
  * already has an account heals to `{ ok: true, existed: true }` — the account
749
- * exists, which is the state apply-time provisioning wanted. */
776
+ * exists, which is the state apply-time provisioning wanted — and, when
777
+ * `publicMetadata` was supplied, refreshes it on the existing account so a
778
+ * re-application repairs a previously missed create (`refreshed: true`). */
750
779
  declare function createClerkUser(secretKey: string, input: ClerkUserInput, fetchImpl?: typeof fetch): Promise<ClerkResult>;
751
780
 
752
781
  /** An application row, as far as the session cares about it. */
@@ -888,6 +917,22 @@ declare const SCHEDULING_DEFAULTS: ResolvedScheduling;
888
917
  * `windowDays` is capped at 62 because Google FreeBusy is.
889
918
  */
890
919
  declare function resolveScheduling(config?: ChapterScheduling): ResolvedScheduling;
920
+ /** Validation messages keyed by form field. */
921
+ type SchedulingErrors = Record<string, string>;
922
+ /**
923
+ * Validate a scheduling config field by field, collecting owner-readable messages
924
+ * rather than throwing on the first problem. An admin settings form needs to say
925
+ * *which* field is wrong and why — "pick at least one day" beats a stack trace —
926
+ * so the admin route returns these directly. {@link resolveScheduling} is the
927
+ * throwing wrapper for internal/config-time use.
928
+ */
929
+ declare function validateScheduling(config?: ChapterScheduling): {
930
+ ok: true;
931
+ value: ResolvedScheduling;
932
+ } | {
933
+ ok: false;
934
+ errors: SchedulingErrors;
935
+ };
891
936
  /** Statuses a member may book/reschedule from (early pipeline only). */
892
937
  /** The availability window: `[now, now + windowDays]` in epoch ms. */
893
938
  declare function slotWindow(now: number, windowDays: number): {
@@ -971,4 +1016,4 @@ type ApplicationBookingPatch = {
971
1016
  * already there (never backward). */
972
1017
  declare function applicationBookingUpdate(currentStatus: string, startAt: number, htmlLink?: string | null): ApplicationBookingPatch;
973
1018
 
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 };
1019
+ 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, 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, validateScheduling, verifyStripeSignature, webhookMutationId };
package/dist/index.js CHANGED
@@ -403,6 +403,17 @@ function resolveApplication(a) {
403
403
  bodyCap: a?.bodyCap ?? 32768
404
404
  };
405
405
  }
406
+ var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
407
+ function applicantProfile(chapter, fields) {
408
+ const profile = {};
409
+ for (const f of [...chapter.application.required, ...chapter.application.optional]) {
410
+ if (IDENTITY_FIELDS.has(f)) continue;
411
+ const v = fields[f];
412
+ if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
413
+ }
414
+ if (fields.focus !== void 0) profile.focus = fields.focus;
415
+ return Object.keys(profile).length > 0 ? profile : void 0;
416
+ }
406
417
  async function submitApplication(db, chapter, fields, opts) {
407
418
  const app = chapter.application;
408
419
  for (const f of app.required) {
@@ -421,6 +432,7 @@ async function submitApplication(db, chapter, fields, opts) {
421
432
  }
422
433
  if (fields.focus !== void 0) row.focus = fields.focus;
423
434
  if (opts.groupId) row.groupId = opts.groupId;
435
+ if (fields.disclaimerAck === true || fields.disclaimerAck === "true") row.disclaimerAckAt = opts.now;
424
436
  const { duplicate } = await db.transact(
425
437
  [{ t: "update", ns: "applications", id: id2, attrs: row }],
426
438
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
@@ -474,6 +486,13 @@ function defineChapter(config) {
474
486
  if (account !== "invite" && account !== "create" && account !== "none") {
475
487
  throw new Error(`defineChapter.account: must be "invite", "create", or "none" \u2014 got "${String(account)}"`);
476
488
  }
489
+ const adminNotification = config.sends?.adminNotification ?? "submit";
490
+ if (adminNotification !== "submit" && adminNotification !== "payment" && adminNotification !== "never") {
491
+ throw new Error(
492
+ `defineChapter.sends.adminNotification: must be "submit", "payment", or "never" \u2014 got "${String(adminNotification)}"`
493
+ );
494
+ }
495
+ const sends = { adminNotification };
477
496
  const chapter = {
478
497
  config,
479
498
  id: id2,
@@ -487,6 +506,7 @@ function defineChapter(config) {
487
506
  rules,
488
507
  services,
489
508
  account,
509
+ sends,
490
510
  groupSeed: () => mode === "chapter" ? buildGroupSeed(config) : null
491
511
  };
492
512
  if (config.url !== void 0) chapter.url = config.url;
@@ -797,6 +817,20 @@ function clerkUserRequest(input) {
797
817
  }
798
818
  };
799
819
  }
820
+ async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
821
+ const auth = { authorization: `Bearer ${secretKey}` };
822
+ const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
823
+ if (!found.ok) return false;
824
+ const users = await found.json().catch(() => null);
825
+ const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
826
+ if (!id2) return false;
827
+ const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id2}/metadata`, {
828
+ method: "PATCH",
829
+ headers: { ...auth, "content-type": "application/json" },
830
+ body: JSON.stringify({ public_metadata: publicMetadata })
831
+ });
832
+ return patched.ok;
833
+ }
800
834
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
801
835
  const { path, body } = clerkUserRequest(input);
802
836
  const res = await fetchImpl(`https://api.clerk.com${path}`, {
@@ -804,7 +838,11 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
804
838
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
805
839
  body: JSON.stringify(body)
806
840
  });
807
- return res.ok ? { ok: true, status: res.status } : heal(res.status);
841
+ if (res.ok) return { ok: true, status: res.status };
842
+ const healed = heal(res.status);
843
+ if (!healed.existed || !input.publicMetadata) return healed;
844
+ const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
845
+ return { ...healed, refreshed };
808
846
  }
809
847
 
810
848
  // src/session.ts
@@ -923,6 +961,12 @@ function isValidTimeZone(tz) {
923
961
  }
924
962
  }
925
963
  function resolveScheduling(config) {
964
+ const result = validateScheduling(config);
965
+ if (result.ok) return result.value;
966
+ const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
967
+ throw new Error(`scheduling: ${detail}`);
968
+ }
969
+ function validateScheduling(config) {
926
970
  const d = config ?? {};
927
971
  const c = {
928
972
  slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
@@ -934,20 +978,29 @@ function resolveScheduling(config) {
934
978
  windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
935
979
  summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
936
980
  };
937
- const fail = (msg) => {
938
- throw new Error(`scheduling: ${msg}`);
939
- };
940
- if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) fail("slotMinutes must be 15\u2013240");
941
- if (!(c.windowDays >= 1 && c.windowDays <= 62)) fail("windowDays must be 1\u201362 (FreeBusy caps at 62)");
942
- if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) fail("minNoticeHours must be 0\u2013336");
943
- if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) fail("require 0 \u2264 startHour < endHour \u2264 24");
981
+ const errors = {};
982
+ if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) {
983
+ errors.slotMinutes = "Slot length must be between 15 and 240 minutes.";
984
+ }
985
+ if (!(c.windowDays >= 1 && c.windowDays <= 62)) {
986
+ errors.windowDays = "Booking window must be between 1 and 62 days (the calendar caps look-ahead at 62).";
987
+ }
988
+ if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) {
989
+ errors.minNoticeHours = "Minimum notice must be between 0 and 336 hours.";
990
+ }
991
+ if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) {
992
+ errors.hours = "Hours must satisfy 0 \u2264 start < end \u2264 24.";
993
+ }
944
994
  const days = [...c.days];
945
- if (!days.length || !days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
946
- fail("days must be a non-empty list of weekday integers 0\u20136");
995
+ if (!days.length) errors.days = "Pick at least one day.";
996
+ else if (!days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
997
+ errors.days = "Days must be weekday numbers, 0 (Sunday) through 6 (Saturday).";
998
+ }
999
+ if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) {
1000
+ errors.timezone = `"${String(c.timezone)}" is not a valid IANA timezone (for example "America/Los_Angeles").`;
947
1001
  }
948
- if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) fail(`invalid IANA timezone "${c.timezone}"`);
949
- if (typeof c.summaryTemplate !== "string") fail("summaryTemplate must be a string");
950
- return { ...c, days };
1002
+ if (typeof c.summaryTemplate !== "string") errors.summaryTemplate = "Calendar summary template must be text.";
1003
+ return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, value: { ...c, days } };
951
1004
  }
952
1005
  function slotWindow(now, windowDays) {
953
1006
  return { from: now, to: now + windowDays * 864e5 };
@@ -996,6 +1049,7 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
996
1049
  }
997
1050
  export {
998
1051
  SCHEDULING_DEFAULTS,
1052
+ applicantProfile,
999
1053
  applicationBookingUpdate,
1000
1054
  applicationSummary,
1001
1055
  bookingDecision,
@@ -1052,6 +1106,7 @@ export {
1052
1106
  stripeForm,
1053
1107
  submitApplication,
1054
1108
  subscriptionIdempotencyKey,
1109
+ validateScheduling,
1055
1110
  verifyStripeSignature,
1056
1111
  webhookMutationId
1057
1112
  };