@odla-ai/chapter 0.9.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/README.md +36 -14
- package/dist/index.cjs +55 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.js +55 -13
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +99 -27
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +17 -0
- package/dist/worker/index.d.ts +17 -0
- package/dist/worker/index.js +99 -27
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -5
package/README.md
CHANGED
|
@@ -135,10 +135,22 @@ These bite silently — a smoke test won't catch them:
|
|
|
135
135
|
- **`services` default** is `["db","calendar","o11y"]`; `smoke` compares config
|
|
136
136
|
against the platform, so set `services` explicitly if you don't run the o11y
|
|
137
137
|
collector.
|
|
138
|
-
- **Which email fires from which route.**
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
- **Which email fires from which route.** Content and addressing are owner-editable
|
|
139
|
+
on the `groups` row; the *trigger* is config:
|
|
140
|
+
|
|
141
|
+
| Template | Fires from | Configurable |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `adminNotification` | `POST /api/applications` (submit) — or the Stripe webhook (first payment) | `sends.adminNotification: "submit" \| "payment" \| "never"` (default `"submit"`) |
|
|
144
|
+
| `prepEmail` | `POST /api/schedule/book` | template `enabled` flag |
|
|
145
|
+
| `paymentConfirmation` | Stripe webhook, first successful invoice | template `enabled` flag |
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
defineChapter({ /* … */ sends: { adminNotification: "payment" } });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Re-check this on every chapter upgrade.** Wiring a send changes a site's
|
|
152
|
+
outbound mail with no local diff — release notes call out send changes
|
|
153
|
+
explicitly for that reason.
|
|
142
154
|
- **Account model.** `account: "invite"` (default) mints a Clerk invitation,
|
|
143
155
|
`"create"` makes the account server-side (so join can say the account is
|
|
144
156
|
ready), `"none"` skips it — all need `clerk_secret_key` in the tenant vault. A
|
|
@@ -153,20 +165,30 @@ These bite silently — a smoke test won't catch them:
|
|
|
153
165
|
|
|
154
166
|
### Install + scope notes
|
|
155
167
|
|
|
156
|
-
- **`@odla-ai/auth-clerk` is
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`@odla-ai/
|
|
168
|
+
- **`@odla-ai/auth-clerk` is not a chapter peer.** It is deliberately absent from
|
|
169
|
+
this package's manifest, because only one entry imports it: the worker verifies
|
|
170
|
+
JWTs with `jose` via `ctx.verifyUser`, and `@odla-ai/chapter/ui/member` is
|
|
171
|
+
react-only. **Install `@odla-ai/auth-clerk` yourself if (and only if) you adopt
|
|
172
|
+
`@odla-ai/chapter/ui/admin`** or want the themed sign-in components; reach for
|
|
160
173
|
`@odla-ai/auth-clerk/invitations` when you want to send your own branded
|
|
161
|
-
invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls
|
|
162
|
-
prefer the narrower entry.
|
|
174
|
+
invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls the admin
|
|
175
|
+
half, so prefer the narrower entry.
|
|
176
|
+
- **Known-good version set** (installs clean, no flags): `@odla-ai/chapter` 0.10.0,
|
|
177
|
+
`@odla-ai/ui` 0.9.0, `@odla-ai/crm` 0.1.4, `@odla-ai/db` 0.6.6,
|
|
178
|
+
`@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1, CLI 0.16.7, plus `jose` and
|
|
179
|
+
react (or `preact` + `preact/compat`). chapter is the package that pulls the
|
|
180
|
+
others together, so this is the line to check when an install complains.
|
|
163
181
|
- **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
|
|
164
182
|
the peer conflict that tells you a pair is unsupported. If you need it, find out
|
|
165
183
|
why first.
|
|
166
|
-
- **The admin surface is intentionally minimal
|
|
167
|
-
`/api/admin/
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
- **The admin surface is intentionally minimal, and staying that way.** chapter
|
|
185
|
+
ships `/api/admin/scheduling` (booking rules, with field-keyed validation
|
|
186
|
+
messages) and `/api/admin/meetings` (the reconciled agenda, with the applicant
|
|
187
|
+
joined, `?all=1`, `from`/`to`, drift fields, Meet/Calendar links and the group
|
|
188
|
+
timezone), plus `peopleSection`. **Dashboard, billing, email config/log/test,
|
|
189
|
+
per-person comms, people list, role changes, approve and refund are out of
|
|
190
|
+
scope** — they are yours as host routes via the seam. Don't plan around them
|
|
191
|
+
shipping.
|
|
170
192
|
|
|
171
193
|
### Verify from the types, not this file
|
|
172
194
|
|
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;
|
|
@@ -881,6 +890,20 @@ function clerkUserRequest(input) {
|
|
|
881
890
|
}
|
|
882
891
|
};
|
|
883
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
|
+
}
|
|
884
907
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
885
908
|
const { path, body } = clerkUserRequest(input);
|
|
886
909
|
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
@@ -888,7 +911,11 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
|
888
911
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
889
912
|
body: JSON.stringify(body)
|
|
890
913
|
});
|
|
891
|
-
|
|
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 };
|
|
892
919
|
}
|
|
893
920
|
|
|
894
921
|
// src/session.ts
|
|
@@ -1007,6 +1034,12 @@ function isValidTimeZone(tz) {
|
|
|
1007
1034
|
}
|
|
1008
1035
|
}
|
|
1009
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) {
|
|
1010
1043
|
const d = config ?? {};
|
|
1011
1044
|
const c = {
|
|
1012
1045
|
slotMinutes: d.slotMinutes ?? SCHEDULING_DEFAULTS.slotMinutes,
|
|
@@ -1018,20 +1051,29 @@ function resolveScheduling(config) {
|
|
|
1018
1051
|
windowDays: d.windowDays ?? SCHEDULING_DEFAULTS.windowDays,
|
|
1019
1052
|
summaryTemplate: d.summaryTemplate ?? SCHEDULING_DEFAULTS.summaryTemplate
|
|
1020
1053
|
};
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
if (!(c.windowDays >= 1 && c.windowDays <= 62))
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
+
}
|
|
1028
1067
|
const days = [...c.days];
|
|
1029
|
-
if (!days.length
|
|
1030
|
-
|
|
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").`;
|
|
1031
1074
|
}
|
|
1032
|
-
if (typeof c.
|
|
1033
|
-
|
|
1034
|
-
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 } };
|
|
1035
1077
|
}
|
|
1036
1078
|
function slotWindow(now, windowDays) {
|
|
1037
1079
|
return { from: now, to: now + windowDays * 864e5 };
|