@odla-ai/chapter 0.10.0 → 0.11.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 +30 -0
- package/dist/index.cjs +24 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +35 -4
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +7 -0
- package/dist/worker/index.d.ts +7 -0
- package/dist/worker/index.js +35 -4
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,6 +156,29 @@ These bite silently — a smoke test won't catch them:
|
|
|
156
156
|
ready), `"none"` skips it — all need `clerk_secret_key` in the tenant vault. A
|
|
157
157
|
site that wants server-side create must set `account: "create"`; inheriting the
|
|
158
158
|
default silently changes the model.
|
|
159
|
+
- **What lands on the Clerk account.** Both models write `public_metadata` as
|
|
160
|
+
`{ applicationId, profile }`, where `profile` is every configured
|
|
161
|
+
`application.required`/`optional` field Clerk doesn't already carry natively
|
|
162
|
+
(so *not* email/firstName/lastName), plus `focus`. It follows your field names —
|
|
163
|
+
`applicantProfile(chapter, fields)` is exported if you want to assert the exact
|
|
164
|
+
shape in a test before deleting a local override.
|
|
165
|
+
- **Disclaimer acknowledgement.** A truthy `disclaimerAck` on the submit body
|
|
166
|
+
(boolean or the string a plain form posts) stamps `disclaimerAckAt` from the
|
|
167
|
+
*server* clock; no ack leaves the attr absent, and a client-supplied
|
|
168
|
+
`disclaimerAckAt` is ignored.
|
|
169
|
+
|
|
170
|
+
Unlike most data loss this is **unreconstructible** — you cannot later
|
|
171
|
+
determine whether someone checked a box — so the submit response always echoes
|
|
172
|
+
`disclaimerAckAt` (a number, or `null` when nothing was recorded). Assert it in
|
|
173
|
+
one integration test and a join page that quietly stops posting the flag can't
|
|
174
|
+
go unnoticed.
|
|
175
|
+
|
|
176
|
+
**If the disclaimer is a compliance record, set
|
|
177
|
+
`application: { requireDisclaimerAck: true }`** and a submit with no ack is a
|
|
178
|
+
400 instead of a row with no consent. It defaults to `false` so an upgrade
|
|
179
|
+
never starts rejecting traffic; enabling it fails *deterministically* on your
|
|
180
|
+
first test submit, not intermittently in production. Expect this to default to
|
|
181
|
+
`true` at 1.0.
|
|
159
182
|
- **CRM projection points.** chapter projects the person on application submit
|
|
160
183
|
(`projectApplicant`), not on booking or on webhook status change. If you mirror
|
|
161
184
|
pipeline stage into the CRM, keep those routes.
|
|
@@ -165,6 +188,13 @@ These bite silently — a smoke test won't catch them:
|
|
|
165
188
|
|
|
166
189
|
### Install + scope notes
|
|
167
190
|
|
|
191
|
+
- **Your first admin is seeded in odla Studio, by hand — and the email must be
|
|
192
|
+
lowercased.** Neither `admins` nor `superAdmins` is ever written by a worker
|
|
193
|
+
route or a provisioning seed; that is deliberate, so nothing running in the app
|
|
194
|
+
(or injected into a page) can grant admin. The gate looks the email up
|
|
195
|
+
lowercased, so a row saved as `Ada@Example.com` matches nothing and the account
|
|
196
|
+
silently isn't an admin — with no error to tell you. Save it lowercase, then
|
|
197
|
+
confirm by signing in, not by looking at the row.
|
|
168
198
|
- **`@odla-ai/auth-clerk` is not a chapter peer.** It is deliberately absent from
|
|
169
199
|
this package's manifest, because only one entry imports it: the worker verifies
|
|
170
200
|
JWTs with `jose` via `ctx.verifyUser`, and `@odla-ai/chapter/ui/member` is
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
24
|
+
applicantProfile: () => applicantProfile,
|
|
24
25
|
applicationBookingUpdate: () => applicationBookingUpdate,
|
|
25
26
|
applicationSummary: () => applicationSummary,
|
|
26
27
|
bookingDecision: () => bookingDecision,
|
|
@@ -44,6 +45,7 @@ __export(index_exports, {
|
|
|
44
45
|
findApplicationRef: () => findApplicationRef,
|
|
45
46
|
firstPaymentPatch: () => firstPaymentPatch,
|
|
46
47
|
getVaultSecret: () => getVaultSecret,
|
|
48
|
+
hasDisclaimerAck: () => hasDisclaimerAck,
|
|
47
49
|
introIdempotencyKey: () => introIdempotencyKey,
|
|
48
50
|
isAdminRole: () => isAdminRole,
|
|
49
51
|
isAlreadySent: () => isAlreadySent,
|
|
@@ -485,9 +487,24 @@ function resolveApplication(a) {
|
|
|
485
487
|
optional,
|
|
486
488
|
maxLen: a?.maxLen ?? {},
|
|
487
489
|
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
488
|
-
bodyCap: a?.bodyCap ?? 32768
|
|
490
|
+
bodyCap: a?.bodyCap ?? 32768,
|
|
491
|
+
requireDisclaimerAck: a?.requireDisclaimerAck ?? false
|
|
489
492
|
};
|
|
490
493
|
}
|
|
494
|
+
function hasDisclaimerAck(fields) {
|
|
495
|
+
return fields.disclaimerAck === true || fields.disclaimerAck === "true";
|
|
496
|
+
}
|
|
497
|
+
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
498
|
+
function applicantProfile(chapter, fields) {
|
|
499
|
+
const profile = {};
|
|
500
|
+
for (const f of [...chapter.application.required, ...chapter.application.optional]) {
|
|
501
|
+
if (IDENTITY_FIELDS.has(f)) continue;
|
|
502
|
+
const v = fields[f];
|
|
503
|
+
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
504
|
+
}
|
|
505
|
+
if (fields.focus !== void 0) profile.focus = fields.focus;
|
|
506
|
+
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
507
|
+
}
|
|
491
508
|
async function submitApplication(db, chapter, fields, opts) {
|
|
492
509
|
const app = chapter.application;
|
|
493
510
|
for (const f of app.required) {
|
|
@@ -499,6 +516,10 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
499
516
|
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
500
517
|
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
501
518
|
}
|
|
519
|
+
const acked = hasDisclaimerAck(fields);
|
|
520
|
+
if (app.requireDisclaimerAck && !acked) {
|
|
521
|
+
return { ok: false, error: "disclaimerAck is required" };
|
|
522
|
+
}
|
|
502
523
|
const id2 = opts.newId();
|
|
503
524
|
const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
504
525
|
for (const f of [...app.required, ...app.optional]) {
|
|
@@ -506,11 +527,12 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
506
527
|
}
|
|
507
528
|
if (fields.focus !== void 0) row.focus = fields.focus;
|
|
508
529
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
530
|
+
if (acked) row.disclaimerAckAt = opts.now;
|
|
509
531
|
const { duplicate } = await db.transact(
|
|
510
532
|
[{ t: "update", ns: "applications", id: id2, attrs: row }],
|
|
511
533
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
512
534
|
);
|
|
513
|
-
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial };
|
|
535
|
+
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
514
536
|
}
|
|
515
537
|
function joinConfig(group, paymentsReady2) {
|
|
516
538
|
return {
|