@odla-ai/chapter 0.13.0 → 0.15.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 +14 -7
- package/dist/index.cjs +81 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.js +81 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,11 +151,15 @@ These bite silently — a smoke test won't catch them:
|
|
|
151
151
|
**Re-check this on every chapter upgrade.** Wiring a send changes a site's
|
|
152
152
|
outbound mail with no local diff — release notes call out send changes
|
|
153
153
|
explicitly for that reason.
|
|
154
|
-
- **Account model
|
|
155
|
-
`"create"` makes the account server-side (so join can say the
|
|
156
|
-
ready), `"
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
- **Account model — the default is `"none"`, and that is deliberate.**
|
|
155
|
+
`account: "create"` makes the Clerk account server-side (so join can say the
|
|
156
|
+
account is ready), `"invite"` **emails the applicant a Clerk invitation**, and
|
|
157
|
+
`"none"` (the default) provisions nothing. Both non-default models need
|
|
158
|
+
`clerk_secret_key` in the tenant vault. The default is side-effect-free on
|
|
159
|
+
purpose: inviting mails a real person, and a site that never made that choice
|
|
160
|
+
must not be sending mail. **You must opt in — a site that wants accounts and
|
|
161
|
+
doesn't set `account` will silently provision none.** (Changed in 0.15.0: the
|
|
162
|
+
default was `"invite"`, which mailed applicants from a config nobody wrote.)
|
|
159
163
|
- **What lands on the Clerk account (and its `public_metadata` is
|
|
160
164
|
client-readable).** Both models write `public_metadata` as
|
|
161
165
|
`{ applicationId, profile }`. By default `profile` is every configured
|
|
@@ -176,8 +180,11 @@ These bite silently — a smoke test won't catch them:
|
|
|
176
180
|
A valid application is never newly rejected.
|
|
177
181
|
- **CRM enrichment.** `projectApplicant` writes the base identity/contact person.
|
|
178
182
|
To carry more of the application into the CRM, list `application: { crmFields:
|
|
179
|
-
[...] }
|
|
180
|
-
|
|
183
|
+
[...] }`. Each name is **cross-checked against your crm `person` type at
|
|
184
|
+
`defineChapter()` time and throws** if it isn't declared there — an undeclared
|
|
185
|
+
field would otherwise be dropped by the projection while the base person still
|
|
186
|
+
lands, making a typo invisible. (The runtime projection still falls back to the
|
|
187
|
+
base person if a write fails, so an applicant is never lost.) Stage mirroring,
|
|
181
188
|
billing snapshots and Clerk-identity linking stay yours as host routes.
|
|
182
189
|
- **Disclaimer acknowledgement.** A truthy `disclaimerAck` on the submit body
|
|
183
190
|
(boolean or the string a plain form posts) stamps `disclaimerAckAt` from the
|
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
clampArray: () => clampArray,
|
|
37
37
|
clerkGetUser: () => clerkGetUser,
|
|
38
38
|
clerkGetUserByEmail: () => clerkGetUserByEmail,
|
|
39
|
+
clerkIntegration: () => clerkIntegration,
|
|
39
40
|
clerkInviteRequest: () => clerkInviteRequest,
|
|
40
41
|
clerkListUsers: () => clerkListUsers,
|
|
41
42
|
clerkSetRole: () => clerkSetRole,
|
|
@@ -606,10 +607,23 @@ function defineChapter(config) {
|
|
|
606
607
|
const application = resolveApplication(config.application);
|
|
607
608
|
const { schema, rules } = chapterDb(mode, auth);
|
|
608
609
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
609
|
-
const account = config.account ?? "
|
|
610
|
+
const account = config.account ?? "none";
|
|
610
611
|
if (account !== "invite" && account !== "create" && account !== "none") {
|
|
611
612
|
throw new Error(`defineChapter.account: must be "invite", "create", or "none" \u2014 got "${String(account)}"`);
|
|
612
613
|
}
|
|
614
|
+
if (application.crmFields.length > 0) {
|
|
615
|
+
let personFields = null;
|
|
616
|
+
try {
|
|
617
|
+
personFields = Object.keys(crm.type("person").fields ?? {});
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
const unknown = personFields ? application.crmFields.filter((f) => !personFields.includes(f)) : [];
|
|
621
|
+
if (unknown.length > 0) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`defineChapter.application.crmFields: ${unknown.map((f) => `"${f}"`).join(", ")} not declared on the CRM "person" type (declared: ${personFields?.join(", ")}). An undeclared field is silently dropped from the CRM projection \u2014 add it to your crm config's person fields, or remove it from crmFields.`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
613
627
|
const adminNotification = config.sends?.adminNotification ?? "submit";
|
|
614
628
|
if (adminNotification !== "submit" && adminNotification !== "payment" && adminNotification !== "never") {
|
|
615
629
|
throw new Error(
|
|
@@ -1023,6 +1037,72 @@ async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
|
|
|
1023
1037
|
return res.ok;
|
|
1024
1038
|
}
|
|
1025
1039
|
|
|
1040
|
+
// src/clerk-integration.ts
|
|
1041
|
+
var clerkIntegration = {
|
|
1042
|
+
id: "clerk",
|
|
1043
|
+
title: "Clerk end-user auth",
|
|
1044
|
+
npm: "@odla-ai/chapter",
|
|
1045
|
+
settings: [
|
|
1046
|
+
{
|
|
1047
|
+
key: "publishableKey",
|
|
1048
|
+
description: "Clerk publishable key (pk_*). Public by design; served to the SPA (loads clerk-js from Clerk's CDN via @odla-ai/auth-clerk).",
|
|
1049
|
+
public: true,
|
|
1050
|
+
pattern: "pk_",
|
|
1051
|
+
perEnv: true,
|
|
1052
|
+
source: "apps-registry auth[env].publishableKey (from odla.config.mjs auth.clerk.<env>)"
|
|
1053
|
+
}
|
|
1054
|
+
],
|
|
1055
|
+
secrets: [
|
|
1056
|
+
{
|
|
1057
|
+
key: "CLERK_WEBHOOK_SECRET",
|
|
1058
|
+
description: "Svix signing secret for Clerk user webhooks \u2014 verifies $users sync events.",
|
|
1059
|
+
pattern: "whsec_",
|
|
1060
|
+
mode: "full",
|
|
1061
|
+
vault: true
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
key: "clerk_secret_key",
|
|
1065
|
+
description: "Clerk backend key (sk_*) \u2014 powers the odla->Clerk writes (account create, role + profile via public_metadata) and lets odla-db resolve user email/name via the Clerk API.",
|
|
1066
|
+
pattern: "sk_",
|
|
1067
|
+
mode: "full",
|
|
1068
|
+
vault: true
|
|
1069
|
+
}
|
|
1070
|
+
],
|
|
1071
|
+
syncs: [
|
|
1072
|
+
{
|
|
1073
|
+
engine: "@odla-ai/db $users (Clerk webhook -> tenant graph)",
|
|
1074
|
+
direction: "provider->odla",
|
|
1075
|
+
entity: "$users",
|
|
1076
|
+
// Matches the webhook mapper: the mirror carries id + primary email + name + avatar.
|
|
1077
|
+
fields: ["id", "email", "name", "imageUrl"],
|
|
1078
|
+
webhook: "whsec_ (svix-signed)",
|
|
1079
|
+
onDelete: "tombstone (never row-delete)"
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
engine: "@odla-ai/chapter clerk.ts (Clerk Backend API via vault clerk_secret_key)",
|
|
1083
|
+
direction: "odla->provider",
|
|
1084
|
+
entity: "clerk user",
|
|
1085
|
+
// Separate merge-PATCHes so a role write never clobbers a profile write.
|
|
1086
|
+
fields: ["public_metadata.role", "public_metadata.profile"],
|
|
1087
|
+
onDelete: "n/a (writes only)"
|
|
1088
|
+
}
|
|
1089
|
+
],
|
|
1090
|
+
provision: {
|
|
1091
|
+
human: [
|
|
1092
|
+
"Run `npx clerk auth login` \u2014 the one interactive step; the agent then creates + configures the Clerk app with the Clerk CLI (no pk_ to hand-paste).",
|
|
1093
|
+
'For auth mode "full": create the Clerk user-sync webhook and store its whsec_, plus the sk_ as clerk_secret_key, in the tenant vault (Studio, write-only).'
|
|
1094
|
+
],
|
|
1095
|
+
cli: [
|
|
1096
|
+
"Clerk CLI (`npx clerk apps create/link/config patch`) creates + configures the instance and pulls the pk_.",
|
|
1097
|
+
"odla `provision` records it: setAuth(env, publishableKey) -> apps-registry (issuer/JWKS derived from the key)."
|
|
1098
|
+
],
|
|
1099
|
+
doctor: [
|
|
1100
|
+
"registry auth[env] present when the app mounts <SignIn>?",
|
|
1101
|
+
'auth mode "full" => whsec_ and clerk_secret_key present in the tenant vault?'
|
|
1102
|
+
]
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1026
1106
|
// src/session.ts
|
|
1027
1107
|
function applicationSummary(app) {
|
|
1028
1108
|
return {
|