@odla-ai/chapter 0.20.0 → 0.20.2
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 +195 -39
- package/package.json +3 -2
- package/runbooks/adopt-existing.md +505 -0
- package/runbooks/greenfield.md +652 -0
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# @odla-ai/chapter
|
|
2
2
|
|
|
3
|
-
A foundation for **membership sites**. One `defineChapter({...})` config
|
|
4
|
-
|
|
5
|
-
member area — plus an admin console and
|
|
3
|
+
A foundation for **membership sites**. One `defineChapter({...})` config resolves
|
|
4
|
+
the shared application engine — join/apply → Stripe membership → Google booking
|
|
5
|
+
→ member area — plus an admin console and CRM, or an **admin-only hub**, on
|
|
6
6
|
odla-db + Clerk + [@odla-ai/crm](https://odla.ai/docs/packages/crm) + calendar +
|
|
7
|
-
email.
|
|
7
|
+
email. The host still builds the public pages, routing, and brand presentation;
|
|
8
|
+
Chapter supplies their application mechanics.
|
|
8
9
|
|
|
9
10
|
```sh
|
|
10
|
-
npm i @odla-ai/chapter
|
|
11
|
+
npm i --save-exact @odla-ai/chapter@0.20.2
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
> **Agentic experiment.** Built and maintained by AI agents from bounded runbooks
|
|
@@ -17,12 +18,32 @@ npm i @odla-ai/chapter
|
|
|
17
18
|
> and leader → follower record delivery ship. APIs may still tighten before 1.0;
|
|
18
19
|
> pin the package and run schema/route contract tests in every site.
|
|
19
20
|
|
|
21
|
+
## Agent runbooks
|
|
22
|
+
|
|
23
|
+
The API examples below are not a website-build plan. An LLM or coding agent must
|
|
24
|
+
choose and read one of these version-matched runbooks from the installed npm
|
|
25
|
+
package before changing a site:
|
|
26
|
+
|
|
27
|
+
- **Empty repo or a genuinely new product:** read
|
|
28
|
+
[`runbooks/greenfield.md`](./runbooks/greenfield.md) completely. It covers the
|
|
29
|
+
brand brief, public information architecture, Chapter configuration,
|
|
30
|
+
member/admin surfaces, Cloudflare/ODLA setup, and visual + journey acceptance.
|
|
31
|
+
- **Existing or deployed site:** read
|
|
32
|
+
[`runbooks/adopt-existing.md`](./runbooks/adopt-existing.md) completely. It
|
|
33
|
+
preserves the existing product, URLs, visual identity, data, auth, jobs, and
|
|
34
|
+
rollback boundary while Chapter becomes the primary implementation.
|
|
35
|
+
|
|
36
|
+
Do not combine the two flows. A provisioned canary is not a completed adoption,
|
|
37
|
+
and a greenfield build does not need migration machinery. Both runbooks keep
|
|
38
|
+
secrets out of source and require development proof before production.
|
|
39
|
+
|
|
20
40
|
## The shape
|
|
21
41
|
|
|
22
42
|
- **One config, two profiles.** `defineChapter()` validates at import and returns
|
|
23
|
-
a resolved engine. `mode: "chapter"`
|
|
24
|
-
"hub"` is admin-only and CRM-focused. The mode
|
|
25
|
-
route surface; everything else (auth, CRM,
|
|
43
|
+
a resolved engine. `mode: "chapter"` enables the complete public-member
|
|
44
|
+
application profile; `mode: "hub"` is admin-only and CRM-focused. The mode
|
|
45
|
+
gates the member/join/payment route surface; everything else (auth, CRM,
|
|
46
|
+
chrome, provisioning) is shared.
|
|
26
47
|
`chapter.brand` and `chapter.network` are resolved from this same config, so
|
|
27
48
|
the browser UI does not need a second identity or follower registry.
|
|
28
49
|
- **The worker is the package.** `chapterWorker({ chapter })` is the whole
|
|
@@ -31,11 +52,15 @@ npm i @odla-ai/chapter
|
|
|
31
52
|
`/api/admin/network/{targets,push}`. Chapter mode adds the public
|
|
32
53
|
member surface (`/api/join-config`, `/api/applications`,
|
|
33
54
|
`/api/schedule/{slots,book}`, `/api/payments/subscription`,
|
|
34
|
-
`/api/webhooks/stripe`)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
`/api/webhooks/stripe`). The `/api/admin/*` handlers are registered in both
|
|
56
|
+
modes; expose only the sections compatible with the selected profile and its
|
|
57
|
+
provisioned namespaces. The Worker then falls back to static assets. Your
|
|
58
|
+
`src/worker.ts` is ~3 lines. Observability is a host concern — wrap it with
|
|
59
|
+
`withObservability` from `@odla-ai/o11y`.
|
|
60
|
+
Operational values that owners may change (prices, policy copy, email
|
|
61
|
+
templates, scheduling rules) are read at runtime from a single odla-db
|
|
62
|
+
`groups` row. Brand identity and build-time tokens remain in the checked-in
|
|
63
|
+
Chapter config.
|
|
39
64
|
- **A route seam, not a black box.** `chapterWorker({ chapter, routes })` runs
|
|
40
65
|
your handlers *before* the built-ins (add routes, or override/alias a path),
|
|
41
66
|
each receiving the same context the built-ins get. The worker entry also exports
|
|
@@ -46,13 +71,15 @@ npm i @odla-ai/chapter
|
|
|
46
71
|
*or* an odla-db `admins` allowlist, plus a read-only `superAdmins` tier —
|
|
47
72
|
selected by `auth.source`, defaulting per mode. Escalation guards
|
|
48
73
|
(`canChangeRole`) are package-enforced.
|
|
49
|
-
- **Correctness is packaged, not per-site.**
|
|
50
|
-
(`sendTemplated`/`isAlreadySent
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
- **Correctness is packaged, not per-site.** Replay-deduplicated email
|
|
75
|
+
(`sendTemplated`/`isAlreadySent`; concurrent sends still need a serialized
|
|
76
|
+
outbox/provider idempotency for a true exactly-once guarantee), a non-prod
|
|
77
|
+
delivery fail-safe (`planDelivery`), status-never-backwards (`canTransition`),
|
|
78
|
+
Stripe webhook integrity (`verifyStripeSignature`) with the webhook as the
|
|
79
|
+
authoritative writer of paid/refunded, one-subscription-per-application
|
|
80
|
+
idempotency, meetings-as-canonical booking (a rebooking *reschedules* the
|
|
81
|
+
event, preserving the Meet link), Google-edit adoption (`reconcileMeetings`),
|
|
82
|
+
and a one-way CRM projection.
|
|
56
83
|
- **Provisioning is declarative.** `createChapterIntegration(chapter)` composes
|
|
57
84
|
the crm namespaces + the chapter namespaces (`applications`, `groups`,
|
|
58
85
|
`meetings`, `emailLog`, plus the auth tables) + a guarded group-row seed. Drop
|
|
@@ -103,7 +130,7 @@ console instead of failing silently. To brand it, pass `brand.palette` /
|
|
|
103
130
|
override the shared visual grammar in both modes. When the `chapter` prop is
|
|
104
131
|
present, `ChapterAdmin` installs those brand tokens itself.
|
|
105
132
|
|
|
106
|
-
##
|
|
133
|
+
## API quick start
|
|
107
134
|
|
|
108
135
|
```ts
|
|
109
136
|
// src/chapter.config.mjs
|
|
@@ -153,11 +180,10 @@ export default {
|
|
|
153
180
|
};
|
|
154
181
|
```
|
|
155
182
|
|
|
156
|
-
That is the reusable application shell.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
account logic to achieve a different brand.
|
|
183
|
+
That is the reusable application shell. Public pages remain site-owned: start
|
|
184
|
+
from the approved product and brand brief plus `@odla-ai/ui` marketing
|
|
185
|
+
components. Do not copy a reference site's identity or fork auth, admin routing,
|
|
186
|
+
CRM, payment, booking, or account logic to achieve a different brand.
|
|
161
187
|
|
|
162
188
|
### Leader → follower delivery
|
|
163
189
|
|
|
@@ -190,13 +216,134 @@ config. The standard collection drawer discovers the target and calls the
|
|
|
190
216
|
admin-gated push route. A follower must declare the receiving CRM type/fields;
|
|
191
217
|
otherwise it rejects the record cleanly instead of dropping fields.
|
|
192
218
|
|
|
219
|
+
#### Existing follower CRMs must opt into the shared graph
|
|
220
|
+
|
|
221
|
+
When `crm` is omitted, chapter's default already contains compatible `person`
|
|
222
|
+
and `company` types plus a `works_at` relation. Passing a custom CRM replaces
|
|
223
|
+
that default; chapter does not merge missing types or fields into it. A follower
|
|
224
|
+
that wants both people and businesses must therefore declare compatible types
|
|
225
|
+
itself:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { defineCrm } from "@odla-ai/crm";
|
|
229
|
+
|
|
230
|
+
const crm = defineCrm({
|
|
231
|
+
types: {
|
|
232
|
+
person: {
|
|
233
|
+
label: "Person",
|
|
234
|
+
labelPlural: "People",
|
|
235
|
+
nameField: "name",
|
|
236
|
+
emailField: "email",
|
|
237
|
+
fields: {
|
|
238
|
+
name: { type: "string", label: "Name", required: true },
|
|
239
|
+
email: { type: "email", label: "Email" },
|
|
240
|
+
firstName: { type: "string", label: "First name" },
|
|
241
|
+
lastName: { type: "string", label: "Last name" },
|
|
242
|
+
phone: { type: "string", label: "Phone" },
|
|
243
|
+
linkedin: { type: "string", label: "LinkedIn" },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
company: {
|
|
247
|
+
label: "Business",
|
|
248
|
+
labelPlural: "Businesses",
|
|
249
|
+
nameField: "name",
|
|
250
|
+
fields: {
|
|
251
|
+
name: { type: "string", label: "Name", required: true },
|
|
252
|
+
domain: { type: "string", label: "Domain / website", slot: "s1" },
|
|
253
|
+
industry: { type: "string", label: "Industry" },
|
|
254
|
+
location: { type: "string", label: "Location" },
|
|
255
|
+
linkedin: { type: "string", label: "LinkedIn" },
|
|
256
|
+
notes: { type: "string", label: "Notes" },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
relations: {
|
|
261
|
+
works_at: {
|
|
262
|
+
from: "person",
|
|
263
|
+
to: "company",
|
|
264
|
+
label: "works at",
|
|
265
|
+
reverseLabel: "team",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The follower must declare every field its leader may send. Unknown types or
|
|
272
|
+
fields fail the request before any CRM write. Record delivery currently moves
|
|
273
|
+
records, not `crm_link` relation rows; create or curate `works_at` links locally.
|
|
274
|
+
|
|
275
|
+
#### Custom leader consoles must mount the sharing UI
|
|
276
|
+
|
|
277
|
+
Automatic “Share with …” actions come from chapter's standard collection
|
|
278
|
+
sections. They are present when the console uses
|
|
279
|
+
`<ChapterAdmin chapter={chapter} />`. Passing an explicit `sections` array
|
|
280
|
+
replaces that catalog, so a custom console must deliberately compose it:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
import {
|
|
284
|
+
ChapterAdmin,
|
|
285
|
+
defaultAdminSections,
|
|
286
|
+
} from "@odla-ai/chapter/ui/admin";
|
|
287
|
+
|
|
288
|
+
const sections = [
|
|
289
|
+
customOverviewSection,
|
|
290
|
+
...defaultAdminSections(chapter),
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
render(
|
|
294
|
+
<ChapterAdmin chapter={chapter} sections={sections} />,
|
|
295
|
+
document.getElementById("admin-root"),
|
|
296
|
+
);
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
If the custom console renders its own record drawer instead, mount
|
|
300
|
+
`NetworkShareActions` inside that drawer:
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
<NetworkShareActions
|
|
304
|
+
recordId={record.id}
|
|
305
|
+
recordType={record.type}
|
|
306
|
+
getToken={sectionContext.getToken}
|
|
307
|
+
/>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The component discovers compatible targets through
|
|
311
|
+
`GET /api/admin/network/targets`; it never receives follower secrets in browser
|
|
312
|
+
data.
|
|
313
|
+
|
|
314
|
+
#### Verify delivery in development before production
|
|
315
|
+
|
|
316
|
+
Use distinct development tenants and follower origins for the first delivery:
|
|
317
|
+
|
|
318
|
+
1. Vault one random value as `network_share_secret` in the follower and under
|
|
319
|
+
the target's resolved `secretName` in the leader.
|
|
320
|
+
2. Confirm `GET /api/admin/network/targets` lists the development follower with
|
|
321
|
+
the expected compatible record types.
|
|
322
|
+
3. Share one test person and one test business from the leader's record drawer.
|
|
323
|
+
Confirm each appears in the follower with only allowlisted fields.
|
|
324
|
+
4. Share each record again. The second delivery must update the same follower
|
|
325
|
+
record, not create a duplicate.
|
|
326
|
+
5. Confirm the follower's pipeline, account, and billing state did not change;
|
|
327
|
+
those remain locally authoritative.
|
|
328
|
+
6. Change one allowlisted leader field and share again to prove later deliveries
|
|
329
|
+
update the existing record. A target with the wrong secret must return `401`
|
|
330
|
+
without writing CRM data.
|
|
331
|
+
|
|
332
|
+
Only after this contract passes against development origins should the leader
|
|
333
|
+
target be changed to a production follower origin and the matching production
|
|
334
|
+
vault values be installed.
|
|
335
|
+
|
|
193
336
|
Section navigation defaults to `/admin/?tab=people`, not
|
|
194
337
|
`/admin/people`. Query routing deliberately works in both leader-style SPAs and
|
|
195
|
-
|
|
338
|
+
ordinary static subdirectories. Legacy path links still open, and
|
|
196
339
|
`routing="path"` remains available only for hosts with an explicit SPA fallback.
|
|
197
340
|
|
|
198
341
|
## Adopting into an existing site
|
|
199
342
|
|
|
343
|
+
For the complete ordered conversion and cutover procedure, use
|
|
344
|
+
[`runbooks/adopt-existing.md`](./runbooks/adopt-existing.md). The notes below
|
|
345
|
+
are the package-specific behavior reference, not a complete adoption plan.
|
|
346
|
+
|
|
200
347
|
A real conversion (the site this was extracted from) went from a 2,094-line
|
|
201
348
|
worker to 6 lines and deleted ~2,500 lines. The order that worked:
|
|
202
349
|
|
|
@@ -302,13 +449,15 @@ These bite silently — a smoke test won't catch them:
|
|
|
302
449
|
|
|
303
450
|
### Install + scope notes
|
|
304
451
|
|
|
305
|
-
- **
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
452
|
+
- **Bootstrap the first admin according to the selected auth source.** Chapter
|
|
453
|
+
mode defaults to Clerk claims: set the first operator's Clerk
|
|
454
|
+
`public_metadata.role` to `admin`. If that operator also needs the
|
|
455
|
+
read-only cross-site tier, add their lowercase email to `superAdmins` in odla
|
|
456
|
+
Studio. Hub mode defaults to table auth: add the first operator's lowercase
|
|
457
|
+
email to `admins` in Studio. Neither allowlist is ever written by a worker
|
|
458
|
+
route or provisioning seed; that is deliberate, so the running app cannot
|
|
459
|
+
grant itself admin. Confirm the result by signing in, not merely by inspecting
|
|
460
|
+
the row.
|
|
312
461
|
- **`@odla-ai/auth-clerk` is not a chapter peer.** It is deliberately absent from
|
|
313
462
|
this package's manifest, because only one entry imports it: the worker verifies
|
|
314
463
|
JWTs with `jose` via `ctx.verifyUser`, and `@odla-ai/chapter/ui/member` is
|
|
@@ -317,11 +466,18 @@ These bite silently — a smoke test won't catch them:
|
|
|
317
466
|
`@odla-ai/auth-clerk/invitations` when you want to send your own branded
|
|
318
467
|
invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls the admin
|
|
319
468
|
half, so prefer the narrower entry.
|
|
320
|
-
- **Known-good
|
|
321
|
-
`@odla-ai/
|
|
322
|
-
`@odla-ai/
|
|
323
|
-
|
|
324
|
-
|
|
469
|
+
- **Known-good application set** (installs clean, no flags):
|
|
470
|
+
`@odla-ai/chapter` 0.20.2, `@odla-ai/ui` 0.10.0, `@odla-ai/crm` 0.1.4,
|
|
471
|
+
`@odla-ai/db` 0.6.6,
|
|
472
|
+
`@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1,
|
|
473
|
+
`@odla-ai/auth-clerk` 0.4.0, `@odla-ai/o11y` 2.2.2, `jose` 6.2.3, React
|
|
474
|
+
19.2.7, and `react-dom` 19.2.7.
|
|
475
|
+
- **Known-good React host toolchain:** `@odla-ai/cli` 0.17.1,
|
|
476
|
+
`@odla-ai/security` 0.3.1, `@cloudflare/workers-types` 4.20260702.1,
|
|
477
|
+
`@types/react` 19.2.17, `@types/react-dom` 19.2.3,
|
|
478
|
+
`@vitejs/plugin-react` 6.0.3, TypeScript 6.0.3, Vite 8.1.4, Vitest 4.1.10,
|
|
479
|
+
and Wrangler 4.107.0. The greenfield runbook standardizes on this exact React
|
|
480
|
+
matrix; a Preact host needs its own tested compatibility set.
|
|
325
481
|
- **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
|
|
326
482
|
the peer conflict that tells you a pair is unsupported. If you need it, find out
|
|
327
483
|
why first.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odla-ai/chapter",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"description": "A leader/follower foundation for branded membership sites: shared CRM, admin, auth, payments, booking, and explicit record delivery from one defineChapter config.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://odla.ai/docs/packages/chapter",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"dist",
|
|
43
|
-
"README.md"
|
|
43
|
+
"README.md",
|
|
44
|
+
"runbooks"
|
|
44
45
|
],
|
|
45
46
|
"sideEffects": false,
|
|
46
47
|
"engines": {
|