@odla-ai/chapter 0.24.0 → 0.25.1
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 +48 -32
- package/dist/{copy-context-Dp5hvj5W.d.ts → copy-context-DI21CYQ3.d.ts} +3 -4
- package/dist/index.cjs +51 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -24
- package/dist/index.d.ts +50 -24
- package/dist/index.js +51 -14
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +211 -22
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +3 -4
- package/dist/worker/index.d.ts +3 -4
- package/dist/worker/index.js +211 -22
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/adopt-existing.md +93 -21
- package/runbooks/greenfield.md +31 -31
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ email. The host still builds the public pages, routing, and brand presentation;
|
|
|
8
8
|
Chapter supplies their application mechanics.
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npm
|
|
11
|
+
npm install @odla-ai/chapter
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
> **Agentic experiment.** Built and maintained by AI agents from bounded runbooks
|
|
@@ -16,7 +16,14 @@ npm i --save-exact @odla-ai/chapter@0.24.0
|
|
|
16
16
|
|
|
17
17
|
> **Pre-1.0.** The member surface, operational worker, standard admin console,
|
|
18
18
|
> and leader → follower record delivery ship. APIs may still tighten before 1.0;
|
|
19
|
-
>
|
|
19
|
+
> commit the lockfile and run schema/route contract tests after every update.
|
|
20
|
+
|
|
21
|
+
> **Ask the runbooks first.** odla's operational procedures live in a database,
|
|
22
|
+
> not in this file: `npx @odla-ai/cli runbook ask "<question>"` returns the current
|
|
23
|
+
> steps, and unlike anything written here it cannot be out of date. Use it before
|
|
24
|
+
> searching the web or working from memory. This README and the JSDoc in the
|
|
25
|
+
> shipped `.d.ts` are the version-matched **API** reference; a runbook is the
|
|
26
|
+
> **procedure**. Most tasks need an answer from both.
|
|
20
27
|
|
|
21
28
|
## Agent runbooks
|
|
22
29
|
|
|
@@ -589,14 +596,18 @@ These bite silently — a smoke test won't catch them:
|
|
|
589
596
|
that omits `account`, so forgetting the decision cannot silently disable or
|
|
590
597
|
enable account provisioning. Both side-effecting models need
|
|
591
598
|
`clerk_secret_key` in the tenant vault. Hub mode resolves to inert `"none"`.
|
|
592
|
-
- **What lands on the Clerk account
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
`application.profileFields`
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
599
|
+
- **What lands on the Clerk account.** `public_metadata` is reserved for small
|
|
600
|
+
browser/session-readable authorization claims such as `role`. Chapter writes
|
|
601
|
+
`applicationId` and the explicitly selected `profile` fields to backend-only
|
|
602
|
+
`private_metadata`; `application.profileFields` defaults to `[]`. Treat that
|
|
603
|
+
copy as a bounded account/admin snapshot—the application row and CRM remain
|
|
604
|
+
canonical. Array fields (`focus`) are clamped to `maxArrayLen` (default 100)
|
|
605
|
+
and non-primitive elements are dropped. `"create"` writes the private snapshot
|
|
606
|
+
with the account. Clerk application invitations cannot carry private
|
|
607
|
+
metadata, so `"invite"` completes the write on the accepted account's first
|
|
608
|
+
`/api/me` request. A one-time application marker prevents repeated Clerk API
|
|
609
|
+
writes. That repair also removes Chapter's former `applicationId` and
|
|
610
|
+
`profile` keys from public metadata without disturbing `role`.
|
|
600
611
|
`applicantProfile(chapter, fields)` is exported to assert the exact shape in a
|
|
601
612
|
test before deleting a local override.
|
|
602
613
|
- **Email + input validation.** The field literally named `email` is checked
|
|
@@ -630,9 +641,14 @@ These bite silently — a smoke test won't catch them:
|
|
|
630
641
|
- **CRM projection points.** chapter projects the person on application submit
|
|
631
642
|
(`projectApplicant`), not on booking or on webhook status change. If you mirror
|
|
632
643
|
pipeline stage into the CRM, keep those routes.
|
|
633
|
-
- **Route names.**
|
|
634
|
-
|
|
635
|
-
|
|
644
|
+
- **Route contracts, not route names.** Chapter serves `/api/config`,
|
|
645
|
+
`/api/join-config`, etc. Alias a legacy URL with a host route only after
|
|
646
|
+
method, auth, request, response shape, units, status, and header parity are
|
|
647
|
+
proven. Equal values with different JSON contracts are not compatible. For a
|
|
648
|
+
join flow, adopt `JoinIsland` end to end or keep an explicit tested adapter;
|
|
649
|
+
do not merely repoint an existing page at `/api/join-config`. `PaymentStep`
|
|
650
|
+
obtains Stripe's `clientSecret`, `publishableKey`, and `lineItems` from
|
|
651
|
+
`POST /api/payments/subscription`, not from the public join-config response.
|
|
636
652
|
|
|
637
653
|
### Install + scope notes
|
|
638
654
|
|
|
@@ -653,25 +669,23 @@ These bite silently — a smoke test won't catch them:
|
|
|
653
669
|
`@odla-ai/auth-clerk/invitations` when you want to send your own branded
|
|
654
670
|
invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls the admin
|
|
655
671
|
half, so prefer the narrower entry.
|
|
656
|
-
- **
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
Preact 10.29.7.
|
|
663
|
-
- **Known-good Preact host toolchain:** `@odla-ai/cli` 0.17.1,
|
|
664
|
-
`@odla-ai/security` 0.3.1, `@cloudflare/workers-types` 4.20260702.1,
|
|
665
|
-
TypeScript 6.0.3, Vite 8.1.4, Vitest 4.1.10, and Wrangler 4.107.0. The
|
|
666
|
-
greenfield runbook standardizes on this exact Preact matrix.
|
|
672
|
+
- **Use normal dependency declarations during active development.** Install
|
|
673
|
+
Chapter, its selected peers, and the Preact host toolchain without exact
|
|
674
|
+
pins. Commit `package-lock.json`, use `npm ci` for repeatable installs, record
|
|
675
|
+
`npm ls` output in PM evidence, and rerun conformance after updates. The
|
|
676
|
+
package manifest's dependency and peer ranges are the compatibility
|
|
677
|
+
contract.
|
|
667
678
|
- **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
|
|
668
679
|
the peer conflict that tells you a pair is unsupported. If you need it, find out
|
|
669
680
|
why first.
|
|
670
681
|
- **The admin operational surface ships (0.16.0).** `/api/admin/*` is now a full
|
|
671
682
|
membership-operations API, admin-gated (`verifyUser` + `isAdmin`):
|
|
672
683
|
- **Roster/identity:** `GET /people` (union of `$users` + applications by email,
|
|
673
|
-
roles from `clerkListUsers`),
|
|
674
|
-
`POST /crm/sync`
|
|
684
|
+
roles and backend-only profile signals from `clerkListUsers`),
|
|
685
|
+
`GET /people/access`, `POST /people/role`, `POST /crm/sync`
|
|
686
|
+
(backfill/reproject), and
|
|
687
|
+
`POST /clerk/private-profiles/sync?offset=0&limit=50` (bounded,
|
|
688
|
+
resumable migration/repair from Chapter's former public profile keys).
|
|
675
689
|
- **Pipeline/meetings:** `GET /dashboard` (flow counts, stage counts + weekly
|
|
676
690
|
delta, agenda, live revenue), `GET /meetings` (reconciled agenda),
|
|
677
691
|
`GET/PUT /scheduling`, `POST /meetings/:id/{reschedule,cancel}`,
|
|
@@ -698,12 +712,14 @@ These bite silently — a smoke test won't catch them:
|
|
|
698
712
|
- **The server-side Clerk primitives are exported too** — for host routes beyond
|
|
699
713
|
the built-in `/people/role`. Beside
|
|
700
714
|
`createClerkUser`/`createClerkInvitation`/`canChangeRole`, chapter exports
|
|
701
|
-
the odla→Clerk write half: `clerkGetUserByEmail`, `clerkGetUser`,
|
|
702
|
-
(auto-paginated — never a silent 100-user cap),
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
715
|
+
the odla→Clerk write half: `clerkGetUserByEmail`, `clerkGetUser`,
|
|
716
|
+
`clerkListUsers` (auto-paginated — never a silent 100-user cap),
|
|
717
|
+
`clerkSetRole`, `updateClerkUserMetadata`, and
|
|
718
|
+
`updateClerkUserMetadataByEmail`. An absent `public_metadata.role` reads as
|
|
719
|
+
the lowest rung (`"provisional"`); normalized `profile` and `applicationId`
|
|
720
|
+
reads come only from backend-only private metadata; and `clerkSetRole`
|
|
721
|
+
merge-`PATCH`es only the public role, so it cannot clobber the private profile.
|
|
722
|
+
All take an injectable `fetch` and the vault `clerk_secret_key`.
|
|
707
723
|
|
|
708
724
|
### Verify from the types, not this file
|
|
709
725
|
|
|
@@ -300,10 +300,9 @@ interface ChapterApplication {
|
|
|
300
300
|
* writing a row with no consent record. Default `true`. Set `false`
|
|
301
301
|
* deliberately only when the site renders no consent control. */
|
|
302
302
|
requireDisclaimerAck?: boolean;
|
|
303
|
-
/** Allowlist of fields
|
|
304
|
-
* `
|
|
305
|
-
*
|
|
306
|
-
* fields the browser may read. */
|
|
303
|
+
/** Allowlist of application fields mirrored into the Clerk account's
|
|
304
|
+
* backend-only `private_metadata.profile`. Default `[]`. Use this for small
|
|
305
|
+
* account/admin signals; the application row and CRM remain canonical. */
|
|
307
306
|
profileFields?: readonly string[];
|
|
308
307
|
/** Extra application fields carried into the one-way CRM projection, on top of
|
|
309
308
|
* the built-in identity/contact set. Each MUST be declared on your crm person
|
package/dist/index.cjs
CHANGED
|
@@ -101,6 +101,8 @@ __export(index_exports, {
|
|
|
101
101
|
submitApplication: () => submitApplication,
|
|
102
102
|
subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
|
|
103
103
|
syncApplicationToCrm: () => syncApplicationToCrm,
|
|
104
|
+
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
105
|
+
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
104
106
|
validateScheduling: () => validateScheduling,
|
|
105
107
|
verifyStripeSignature: () => verifyStripeSignature,
|
|
106
108
|
webhookMutationId: () => webhookMutationId
|
|
@@ -153,6 +155,10 @@ var applications = {
|
|
|
153
155
|
meetingAt: attr("number", { indexed: true, optional: true }),
|
|
154
156
|
meetingLink: attr("string", { optional: true }),
|
|
155
157
|
clerkUserId: attr("string", { indexed: true, optional: true }),
|
|
158
|
+
// One-time/effect-repair marker. Absent means /api/me should copy the
|
|
159
|
+
// allowlisted application snapshot into Clerk private_metadata and remove
|
|
160
|
+
// the former public_metadata copy.
|
|
161
|
+
clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
|
|
156
162
|
phone: attr("string", { optional: true }),
|
|
157
163
|
state: attr("string", { optional: true }),
|
|
158
164
|
groupId: attr("string", { indexed: true, optional: true }),
|
|
@@ -1611,23 +1617,30 @@ function clerkUserRequest(input) {
|
|
|
1611
1617
|
skip_password_requirement: true,
|
|
1612
1618
|
...input.firstName ? { first_name: input.firstName } : {},
|
|
1613
1619
|
...input.lastName ? { last_name: input.lastName } : {},
|
|
1614
|
-
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
1620
|
+
...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
|
|
1621
|
+
...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
|
|
1615
1622
|
}
|
|
1616
1623
|
};
|
|
1617
1624
|
}
|
|
1618
|
-
async function
|
|
1625
|
+
async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fetch) {
|
|
1626
|
+
const res = await fetchImpl(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}/metadata`, {
|
|
1627
|
+
method: "PATCH",
|
|
1628
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
1629
|
+
body: JSON.stringify({
|
|
1630
|
+
...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
|
|
1631
|
+
...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
|
|
1632
|
+
})
|
|
1633
|
+
});
|
|
1634
|
+
return res.ok;
|
|
1635
|
+
}
|
|
1636
|
+
async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
|
|
1619
1637
|
const auth = { authorization: `Bearer ${secretKey}` };
|
|
1620
1638
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
1621
1639
|
if (!found.ok) return false;
|
|
1622
1640
|
const users = await found.json().catch(() => null);
|
|
1623
1641
|
const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
1624
1642
|
if (!id2) return false;
|
|
1625
|
-
|
|
1626
|
-
method: "PATCH",
|
|
1627
|
-
headers: { ...auth, "content-type": "application/json" },
|
|
1628
|
-
body: JSON.stringify({ public_metadata: publicMetadata })
|
|
1629
|
-
});
|
|
1630
|
-
return patched.ok;
|
|
1643
|
+
return updateClerkUserMetadata(secretKey, id2, input, fetchImpl);
|
|
1631
1644
|
}
|
|
1632
1645
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
1633
1646
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -1638,8 +1651,15 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
|
1638
1651
|
});
|
|
1639
1652
|
if (res.ok) return { ok: true, status: res.status };
|
|
1640
1653
|
const healed = heal(res.status);
|
|
1641
|
-
|
|
1642
|
-
|
|
1654
|
+
const repair = input.repairMetadata ?? {
|
|
1655
|
+
...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
|
|
1656
|
+
...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
|
|
1657
|
+
};
|
|
1658
|
+
if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
|
|
1659
|
+
const refreshed = await updateClerkUserMetadataByEmail(secretKey, input.email, {
|
|
1660
|
+
...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
|
|
1661
|
+
...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
|
|
1662
|
+
}, fetchImpl).catch(() => false);
|
|
1643
1663
|
return { ...healed, refreshed };
|
|
1644
1664
|
}
|
|
1645
1665
|
|
|
@@ -1650,9 +1670,21 @@ var PAGE = 100;
|
|
|
1650
1670
|
function toRecord(u) {
|
|
1651
1671
|
if (typeof u.id !== "string") return null;
|
|
1652
1672
|
const pm = u.public_metadata ?? {};
|
|
1673
|
+
const privateMetadata = u.private_metadata ?? {};
|
|
1674
|
+
const rawProfile = privateMetadata.profile;
|
|
1675
|
+
const profile = rawProfile && typeof rawProfile === "object" && !Array.isArray(rawProfile) ? rawProfile : {};
|
|
1676
|
+
const applicationId = typeof privateMetadata.applicationId === "string" ? privateMetadata.applicationId : void 0;
|
|
1653
1677
|
const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
|
|
1654
1678
|
const email = u.email_addresses?.[0]?.email_address;
|
|
1655
|
-
return {
|
|
1679
|
+
return {
|
|
1680
|
+
id: u.id,
|
|
1681
|
+
email: typeof email === "string" ? email : void 0,
|
|
1682
|
+
role,
|
|
1683
|
+
publicMetadata: pm,
|
|
1684
|
+
privateMetadata,
|
|
1685
|
+
profile,
|
|
1686
|
+
...applicationId ? { applicationId } : {}
|
|
1687
|
+
};
|
|
1656
1688
|
}
|
|
1657
1689
|
async function clerkGet(path, secretKey, fetchImpl) {
|
|
1658
1690
|
const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
|
|
@@ -1715,7 +1747,7 @@ var clerkIntegration = {
|
|
|
1715
1747
|
},
|
|
1716
1748
|
{
|
|
1717
1749
|
key: "clerk_secret_key",
|
|
1718
|
-
description: "Clerk backend key (sk_*) \u2014 powers the
|
|
1750
|
+
description: "Clerk backend key (sk_*) \u2014 powers account creation, the public role claim, and backend-only private profile signals; it also lets odla-db resolve user email/name via the Clerk API.",
|
|
1719
1751
|
pattern: "sk_",
|
|
1720
1752
|
mode: "full",
|
|
1721
1753
|
vault: true
|
|
@@ -1735,8 +1767,13 @@ var clerkIntegration = {
|
|
|
1735
1767
|
engine: "@odla-ai/chapter clerk.ts (Clerk Backend API via vault clerk_secret_key)",
|
|
1736
1768
|
direction: "odla->provider",
|
|
1737
1769
|
entity: "clerk user",
|
|
1738
|
-
// Separate
|
|
1739
|
-
|
|
1770
|
+
// Separate metadata fields so a public role write cannot expose or
|
|
1771
|
+
// clobber backend-only account/profile signals.
|
|
1772
|
+
fields: [
|
|
1773
|
+
"public_metadata.role",
|
|
1774
|
+
"private_metadata.applicationId",
|
|
1775
|
+
"private_metadata.profile"
|
|
1776
|
+
],
|
|
1740
1777
|
onDelete: "n/a (writes only)"
|
|
1741
1778
|
}
|
|
1742
1779
|
],
|