@odla-ai/chapter 0.14.0 → 0.15.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 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.** `account: "invite"` (default) mints a Clerk invitation,
155
- `"create"` makes the account server-side (so join can say the account is
156
- ready), `"none"` skips it all need `clerk_secret_key` in the tenant vault. A
157
- site that wants server-side create must set `account: "create"`; inheriting the
158
- default silently changes the model.
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
- [...] }` each must be a field on your crm person type, else that field is
180
- dropped and the base person still projects (it is never lost). Stage mirroring,
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
@@ -607,10 +607,23 @@ function defineChapter(config) {
607
607
  const application = resolveApplication(config.application);
608
608
  const { schema, rules } = chapterDb(mode, auth);
609
609
  const services = config.services ?? ["db", "calendar", "o11y"];
610
- const account = config.account ?? "invite";
610
+ const account = config.account ?? "none";
611
611
  if (account !== "invite" && account !== "create" && account !== "none") {
612
612
  throw new Error(`defineChapter.account: must be "invite", "create", or "none" \u2014 got "${String(account)}"`);
613
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
+ }
614
627
  const adminNotification = config.sends?.adminNotification ?? "submit";
615
628
  if (adminNotification !== "submit" && adminNotification !== "payment" && adminNotification !== "never") {
616
629
  throw new Error(