@odla-ai/chapter 0.7.0 → 0.8.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 +61 -13
- package/dist/index.cjs +30 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.js +30 -3
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +29 -4
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +9 -0
- package/dist/worker/index.d.ts +9 -0
- package/dist/worker/index.js +29 -4
- package/dist/worker/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -97,18 +97,66 @@ export default {
|
|
|
97
97
|
|
|
98
98
|
## Adopting into an existing site
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
A real conversion (the site this was extracted from) went from a 2,094-line
|
|
101
|
+
worker to 6 lines and deleted ~2,500 lines. The order that worked:
|
|
102
|
+
|
|
103
|
+
1. **Config first, assert parity BEFORE deleting anything.** `defineChapter()`
|
|
104
|
+
your site, then diff `chapter.schema` against your existing schema in a test
|
|
105
|
+
and require byte-equality. That single assertion is what makes the deletion
|
|
106
|
+
safe rather than hopeful.
|
|
107
|
+
2. **Freeze the old schema as a test fixture** (e.g. `test/fixtures/legacy-schema.mjs`)
|
|
108
|
+
and keep asserting against it, so an upstream default change fails a test
|
|
109
|
+
instead of a live `provision`. It is the only durable guard against drift in a
|
|
110
|
+
generated schema.
|
|
111
|
+
3. **Swap provisioning** — `createChapterIntegration(chapter)`, inert until the
|
|
112
|
+
next provision run. It supplies schema + rules + seeds, so your
|
|
113
|
+
`odla.config.mjs` `db` block goes away entirely.
|
|
114
|
+
4. **Then the worker**, keeping every bespoke route as a host route
|
|
115
|
+
(`chapterWorker({ chapter, routes })`). Don't hand routes to chapter in the
|
|
116
|
+
same change as the framework swap.
|
|
117
|
+
5. **Override rather than inherit** wherever local behavior was a decision.
|
|
118
|
+
|
|
119
|
+
### Behavior deltas to audit
|
|
120
|
+
|
|
121
|
+
These bite silently — a smoke test won't catch them:
|
|
122
|
+
|
|
123
|
+
- **Per-field caps.** `application.defaultMaxLen` is 2000. If your form accepts
|
|
124
|
+
longer input, pass `maxLen` explicitly or the default starts rejecting it.
|
|
125
|
+
- **`services` default** is `["db","calendar","o11y"]`; `smoke` compares config
|
|
126
|
+
against the platform, so set `services` explicitly if you don't run the o11y
|
|
127
|
+
collector.
|
|
128
|
+
- **Which email fires from which route.** `adminNotification` fires from
|
|
129
|
+
`POST /api/applications` (on submit), `prepEmail` from `/api/schedule/book`,
|
|
130
|
+
`paymentConfirmation` from the Stripe webhook. If your policy differs (e.g.
|
|
131
|
+
notify on payment, not submit), override that route.
|
|
132
|
+
- **Account model.** `account: "invite"` (default) mints a Clerk invitation,
|
|
133
|
+
`"create"` makes the account server-side (so join can say the account is
|
|
134
|
+
ready), `"none"` skips it — all need `clerk_secret_key` in the tenant vault. A
|
|
135
|
+
site that wants server-side create must set `account: "create"`; inheriting the
|
|
136
|
+
default silently changes the model.
|
|
137
|
+
- **CRM projection points.** chapter projects the person on application submit
|
|
138
|
+
(`projectApplicant`), not on booking or on webhook status change. If you mirror
|
|
139
|
+
pipeline stage into the CRM, keep those routes.
|
|
140
|
+
- **Route names.** chapter serves `/api/config`, `/api/join-config`, etc. Alias
|
|
141
|
+
legacy names in ~4 lines with a host route in `chapterWorker({ routes })` rather
|
|
142
|
+
than rewriting pages.
|
|
143
|
+
|
|
144
|
+
### Install + scope notes
|
|
145
|
+
|
|
146
|
+
- **Skip `@odla-ai/auth-clerk` for a worker-only adoption.** The worker never
|
|
147
|
+
imports it (JWTs are verified with `jose` via `ctx.verifyUser`); it is an
|
|
148
|
+
optional peer for `chapter/ui` only. Reach for `@odla-ai/auth-clerk/invitations`
|
|
149
|
+
when you want to send your own branded invitation mail.
|
|
150
|
+
- **The admin surface is intentionally minimal** — `/api/admin/scheduling` +
|
|
151
|
+
`/api/admin/meetings`, plus `peopleSection`. Dashboard, billing, per-person
|
|
152
|
+
comms, approve/refund, etc. are your own host routes via the seam; don't plan
|
|
153
|
+
around them shipping.
|
|
154
|
+
|
|
155
|
+
### Verify from the types, not this file
|
|
156
|
+
|
|
157
|
+
At several releases a day, prose lags. Treat this README as intent and verify the
|
|
158
|
+
real surface from `dist/*.d.ts` and by grepping the built bundle for route
|
|
159
|
+
strings. One testing gotcha: Clerk session tokens expire in ~60s, so a script
|
|
160
|
+
that mints a JWT then runs a batch of curls must re-mint per batch.
|
|
113
161
|
|
|
114
162
|
MIT © odla
|
package/dist/index.cjs
CHANGED
|
@@ -33,8 +33,10 @@ __export(index_exports, {
|
|
|
33
33
|
canceledPatch: () => canceledPatch,
|
|
34
34
|
chapterDb: () => chapterDb,
|
|
35
35
|
clerkInviteRequest: () => clerkInviteRequest,
|
|
36
|
+
clerkUserRequest: () => clerkUserRequest,
|
|
36
37
|
createChapterIntegration: () => createChapterIntegration,
|
|
37
38
|
createClerkInvitation: () => createClerkInvitation,
|
|
39
|
+
createClerkUser: () => createClerkUser,
|
|
38
40
|
defaultCrm: () => defaultCrm,
|
|
39
41
|
defineChapter: () => defineChapter,
|
|
40
42
|
emailGroupFrom: () => emailGroupFrom,
|
|
@@ -308,7 +310,7 @@ Warmly,
|
|
|
308
310
|
${name}`;
|
|
309
311
|
return {
|
|
310
312
|
adminNotification: {
|
|
311
|
-
subject: `New application
|
|
313
|
+
subject: `New application: {{firstName}} {{lastName}}`,
|
|
312
314
|
text: `A new application came in for ${name}.
|
|
313
315
|
|
|
314
316
|
Name: {{firstName}} {{lastName}}
|
|
@@ -327,7 +329,7 @@ Your membership payment is confirmed. We'll be in touch to schedule your intro c
|
|
|
327
329
|
Looking forward to our call at {{meetingTime}}. {{meetingLink}}${sign}`
|
|
328
330
|
},
|
|
329
331
|
onboardingInvite: {
|
|
330
|
-
subject: `You're in
|
|
332
|
+
subject: `You're in, ${name}`,
|
|
331
333
|
text: `Hi {{firstName}},
|
|
332
334
|
|
|
333
335
|
Welcome to ${name}. Your member area is here: {{membersUrl}}${sign}`
|
|
@@ -552,6 +554,10 @@ function defineChapter(config) {
|
|
|
552
554
|
const application = resolveApplication(config.application);
|
|
553
555
|
const { schema, rules } = chapterDb(mode, auth);
|
|
554
556
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
557
|
+
const account = config.account ?? "invite";
|
|
558
|
+
if (account !== "invite" && account !== "create" && account !== "none") {
|
|
559
|
+
throw new Error(`defineChapter.account: must be "invite", "create", or "none" \u2014 got "${String(account)}"`);
|
|
560
|
+
}
|
|
555
561
|
const chapter = {
|
|
556
562
|
config,
|
|
557
563
|
id: id2,
|
|
@@ -564,6 +570,7 @@ function defineChapter(config) {
|
|
|
564
570
|
schema,
|
|
565
571
|
rules,
|
|
566
572
|
services,
|
|
573
|
+
account,
|
|
567
574
|
groupSeed: () => mode === "chapter" ? buildGroupSeed(config) : null
|
|
568
575
|
};
|
|
569
576
|
if (config.url !== void 0) chapter.url = config.url;
|
|
@@ -590,7 +597,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
590
597
|
}
|
|
591
598
|
return {
|
|
592
599
|
id: "chapter",
|
|
593
|
-
title: `Chapter
|
|
600
|
+
title: `Chapter: ${chapter.name}`,
|
|
594
601
|
npm: "@odla-ai/chapter",
|
|
595
602
|
schema: {
|
|
596
603
|
entities: { ...crmDesc.schema.entities, ...chapter.schema.entities },
|
|
@@ -860,6 +867,26 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
|
860
867
|
});
|
|
861
868
|
return { ok: res.ok, status: res.status };
|
|
862
869
|
}
|
|
870
|
+
function clerkUserRequest(input) {
|
|
871
|
+
return {
|
|
872
|
+
path: "/v1/users",
|
|
873
|
+
body: {
|
|
874
|
+
email_address: [input.email],
|
|
875
|
+
skip_password_requirement: true,
|
|
876
|
+
...input.firstName ? { first_name: input.firstName } : {},
|
|
877
|
+
...input.lastName ? { last_name: input.lastName } : {}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
882
|
+
const { path, body } = clerkUserRequest(input);
|
|
883
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
884
|
+
method: "POST",
|
|
885
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
886
|
+
body: JSON.stringify(body)
|
|
887
|
+
});
|
|
888
|
+
return { ok: res.ok, status: res.status };
|
|
889
|
+
}
|
|
863
890
|
|
|
864
891
|
// src/session.ts
|
|
865
892
|
function applicationSummary(app) {
|