@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 CHANGED
@@ -1,13 +1,14 @@
1
1
  # @odla-ai/chapter
2
2
 
3
- A foundation for **membership sites**. One `defineChapter({...})` config stands up
4
- a full public member site — join/apply → Stripe membership → Google booking
5
- member area — plus an admin console and a CRM, or an **admin-only hub**, on
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"` is the full public member site; `mode:
24
- "hub"` is admin-only and CRM-focused. The mode gates the member/join/payment
25
- route surface; everything else (auth, CRM, chrome, provisioning) is shared.
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`) and the admin operational routes (`/api/admin/*`), then
35
- falls back to static assets. Your `src/worker.ts` is ~3 lines. Observability is
36
- a host concern wrap it with `withObservability` from `@odla-ai/o11y`.
37
- Everything brand-specific (prices, policy copy, email templates, scheduling
38
- rules) is read at runtime from a single odla-db `groups` row, never hardcoded.
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.** Exactly-once email
50
- (`sendTemplated`/`isAlreadySent`), a non-prod delivery fail-safe
51
- (`planDelivery`), status-never-backwards (`canTransition`), Stripe webhook
52
- integrity (`verifyStripeSignature`) with the webhook as the authoritative writer
53
- of paid/refunded, one-subscription-per-application idempotency, meetings-as-
54
- canonical booking (a rebooking *reschedules* the event, preserving the Meet
55
- link), Google-edit adoption (`reconcileMeetings`), and a one-way CRM projection.
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
- ## Quick start
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. The follower's public pages remain
157
- site-owned: start from the Silver & Salt content rhythm and `@odla-ai/ui`
158
- marketing components, then change the wordmark, palette, copy, imagery, and
159
- investment thesis. Do not fork auth, admin routing, CRM, payment, booking, or
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
- Silver & Salt-style static subdirectories. Legacy path links still open, and
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
- - **Your first admin is seeded in odla Studio, by hand — and the email must be
306
- lowercased.** Neither `admins` nor `superAdmins` is ever written by a worker
307
- route or a provisioning seed; that is deliberate, so nothing running in the app
308
- (or injected into a page) can grant admin. The gate looks the email up
309
- lowercased, so a row saved as `Ada@Example.com` matches nothing and the account
310
- silently isn't an admin with no error to tell you. Save it lowercase, then
311
- confirm by signing in, not by looking at the row.
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 version set** (installs clean, no flags): `@odla-ai/chapter` 0.20.0,
321
- `@odla-ai/ui` 0.10.0, `@odla-ai/crm` 0.1.4, `@odla-ai/db` 0.6.6,
322
- `@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1, CLI 0.17.1, plus `jose` and
323
- react (or `preact` + `preact/compat`). chapter is the package that pulls the
324
- others together, so this is the line to check when an install complains.
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.0",
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": {