@odla-ai/chapter 0.8.0 → 0.10.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.cjs CHANGED
@@ -77,6 +77,7 @@ __export(index_exports, {
77
77
  stripeForm: () => stripeForm,
78
78
  submitApplication: () => submitApplication,
79
79
  subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
80
+ validateScheduling: () => validateScheduling,
80
81
  verifyStripeSignature: () => verifyStripeSignature,
81
82
  webhookMutationId: () => webhookMutationId
82
83
  });
@@ -558,6 +559,13 @@ function defineChapter(config) {
558
559
  if (account !== "invite" && account !== "create" && account !== "none") {
559
560
  throw new Error(`defineChapter.account: must be "invite", "create", or "none" \u2014 got "${String(account)}"`);
560
561
  }
562
+ const adminNotification = config.sends?.adminNotification ?? "submit";
563
+ if (adminNotification !== "submit" && adminNotification !== "payment" && adminNotification !== "never") {
564
+ throw new Error(
565
+ `defineChapter.sends.adminNotification: must be "submit", "payment", or "never" \u2014 got "${String(adminNotification)}"`
566
+ );
567
+ }
568
+ const sends = { adminNotification };
561
569
  const chapter = {
562
570
  config,
563
571
  id: id2,
@@ -571,6 +579,7 @@ function defineChapter(config) {
571
579
  rules,
572
580
  services,
573
581
  account,
582
+ sends,
574
583
  groupSeed: () => mode === "chapter" ? buildGroupSeed(config) : null
575
584
  };
576
585
  if (config.url !== void 0) chapter.url = config.url;
@@ -848,13 +857,15 @@ async function projectApplicant(deps, applicant) {
848
857
  }
849
858
 
850
859
  // src/clerk.ts
860
+ var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
851
861
  function clerkInviteRequest(input) {
852
862
  return {
853
863
  path: "/v1/invitations",
854
864
  body: {
855
865
  email_address: input.email,
856
866
  notify: true,
857
- ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {}
867
+ ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
868
+ ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
858
869
  }
859
870
  };
860
871
  }
@@ -865,7 +876,7 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
865
876
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
866
877
  body: JSON.stringify(body)
867
878
  });
868
- return { ok: res.ok, status: res.status };
879
+ return res.ok ? { ok: true, status: res.status } : heal(res.status);
869
880
  }
870
881
  function clerkUserRequest(input) {
871
882
  return {
@@ -874,10 +885,25 @@ function clerkUserRequest(input) {
874
885
  email_address: [input.email],
875
886
  skip_password_requirement: true,
876
887
  ...input.firstName ? { first_name: input.firstName } : {},
877
- ...input.lastName ? { last_name: input.lastName } : {}
888
+ ...input.lastName ? { last_name: input.lastName } : {},
889
+ ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
878
890
  }
879
891
  };
880
892
  }
893
+ async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
894
+ const auth = { authorization: `Bearer ${secretKey}` };
895
+ const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
896
+ if (!found.ok) return false;
897
+ const users = await found.json().catch(() => null);
898
+ const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
899
+ if (!id2) return false;
900
+ const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id2}/metadata`, {
901
+ method: "PATCH",
902
+ headers: { ...auth, "content-type": "application/json" },
903
+ body: JSON.stringify({ public_metadata: publicMetadata })
904
+ });
905
+ return patched.ok;
906
+ }
881
907
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
882
908
  const { path, body } = clerkUserRequest(input);
883
909
  const res = await fetchImpl(`https://api.clerk.com${path}`, {
@@ -885,7 +911,11 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
885
911
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
886
912
  body: JSON.stringify(body)
887
913
  });
888
- return { ok: res.ok, status: res.status };
914
+ if (res.ok) return { ok: true, status: res.status };
915
+ const healed = heal(res.status);
916
+ if (!healed.existed || !input.publicMetadata) return healed;
917
+ const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
918
+ return { ...healed, refreshed };
889
919
  }
890
920
 
891
921
  // src/session.ts
@@ -1004,6 +1034,12 @@ function isValidTimeZone(tz) {
1004
1034
  }
1005
1035
  }
1006
1036
  function resolveScheduling(config) {
1037
+ const result = validateScheduling(config);
1038
+ if (result.ok) return result.value;
1039
+ const detail = Object.entries(result.errors).map(([field, message]) => `${field}: ${message}`).join(" ");
1040
+ throw new Error(`scheduling: ${detail}`);
1041
+ }
1042
+ function validateScheduling(config) {
1007
1043
  const d = config ?? {};
1008
1044
  const c = {
1009
1045
  slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
@@ -1015,20 +1051,29 @@ function resolveScheduling(config) {
1015
1051
  windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
1016
1052
  summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
1017
1053
  };
1018
- const fail = (msg) => {
1019
- throw new Error(`scheduling: ${msg}`);
1020
- };
1021
- if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) fail("slotMinutes must be 15\u2013240");
1022
- if (!(c.windowDays >= 1 && c.windowDays <= 62)) fail("windowDays must be 1\u201362 (FreeBusy caps at 62)");
1023
- if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) fail("minNoticeHours must be 0\u2013336");
1024
- if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) fail("require 0 \u2264 startHour < endHour \u2264 24");
1054
+ const errors = {};
1055
+ if (!(c.slotMinutes >= 15 && c.slotMinutes <= 240)) {
1056
+ errors.slotMinutes = "Slot length must be between 15 and 240 minutes.";
1057
+ }
1058
+ if (!(c.windowDays >= 1 && c.windowDays <= 62)) {
1059
+ errors.windowDays = "Booking window must be between 1 and 62 days (the calendar caps look-ahead at 62).";
1060
+ }
1061
+ if (!(c.minNoticeHours >= 0 && c.minNoticeHours <= 336)) {
1062
+ errors.minNoticeHours = "Minimum notice must be between 0 and 336 hours.";
1063
+ }
1064
+ if (!(c.startHour >= 0 && c.startHour < c.endHour && c.endHour <= 24)) {
1065
+ errors.hours = "Hours must satisfy 0 \u2264 start < end \u2264 24.";
1066
+ }
1025
1067
  const days = [...c.days];
1026
- if (!days.length || !days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
1027
- fail("days must be a non-empty list of weekday integers 0\u20136");
1068
+ if (!days.length) errors.days = "Pick at least one day.";
1069
+ else if (!days.every((n) => Number.isInteger(n) && n >= 0 && n <= 6)) {
1070
+ errors.days = "Days must be weekday numbers, 0 (Sunday) through 6 (Saturday).";
1071
+ }
1072
+ if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) {
1073
+ errors.timezone = `"${String(c.timezone)}" is not a valid IANA timezone (for example "America/Los_Angeles").`;
1028
1074
  }
1029
- if (typeof c.timezone !== "string" || !isValidTimeZone(c.timezone)) fail(`invalid IANA timezone "${c.timezone}"`);
1030
- if (typeof c.summaryTemplate !== "string") fail("summaryTemplate must be a string");
1031
- return { ...c, days };
1075
+ if (typeof c.summaryTemplate !== "string") errors.summaryTemplate = "Calendar summary template must be text.";
1076
+ return Object.keys(errors).length > 0 ? { ok: false, errors } : { ok: true, value: { ...c, days } };
1032
1077
  }
1033
1078
  function slotWindow(now, windowDays) {
1034
1079
  return { from: now, to: now + windowDays * 864e5 };