@odla-ai/chapter 0.11.0 → 0.12.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 +27 -5
- package/dist/index.cjs +39 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -7
- package/dist/index.d.ts +47 -7
- package/dist/index.js +39 -7
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +31 -7
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +25 -0
- package/dist/worker/index.d.ts +25 -0
- package/dist/worker/index.js +31 -7
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,12 +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
|
|
160
|
-
|
|
159
|
+
- **What lands on the Clerk account (and its `public_metadata` is
|
|
160
|
+
client-readable).** Both models write `public_metadata` as
|
|
161
|
+
`{ applicationId, profile }`. By default `profile` is every configured
|
|
161
162
|
`application.required`/`optional` field Clerk doesn't already carry natively
|
|
162
|
-
(so *not* email/firstName/lastName), plus `focus
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
(so *not* email/firstName/lastName), plus `focus` — **which means free-text or
|
|
164
|
+
third-party fields like `message` or `referral` are exposed to the browser
|
|
165
|
+
unless you curate.** Set `application: { profileFields: ["phone", "state",
|
|
166
|
+
"focus", ...] }` to an allowlist and everything else stays db-only. Array fields
|
|
167
|
+
(`focus`) are clamped to `maxArrayLen` (default 100) and non-primitive elements
|
|
168
|
+
dropped, so a client can't post an unbounded array into metadata.
|
|
169
|
+
`applicantProfile(chapter, fields)` is exported to assert the exact shape in a
|
|
170
|
+
test before deleting a local override. (Default is back-compat today; expected
|
|
171
|
+
to tighten at 1.0.)
|
|
172
|
+
- **Email + input validation.** The field literally named `email` is checked
|
|
173
|
+
against a permissive email shape (400 on `"notanemail"`, so it fails cleanly
|
|
174
|
+
here rather than at the downstream Clerk create) — set
|
|
175
|
+
`application: { validateEmail: false }` to opt out; `isValidEmail` is exported.
|
|
176
|
+
A valid application is never newly rejected.
|
|
177
|
+
- **CRM enrichment.** `projectApplicant` writes the base identity/contact person.
|
|
178
|
+
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,
|
|
181
|
+
billing snapshots and Clerk-identity linking stay yours as host routes.
|
|
165
182
|
- **Disclaimer acknowledgement.** A truthy `disclaimerAck` on the submit body
|
|
166
183
|
(boolean or the string a plain form posts) stamps `disclaimerAckAt` from the
|
|
167
184
|
*server* clock; no ack leaves the attr absent, and a client-supplied
|
|
@@ -219,6 +236,11 @@ These bite silently — a smoke test won't catch them:
|
|
|
219
236
|
per-person comms, people list, role changes, approve and refund are out of
|
|
220
237
|
scope** — they are yours as host routes via the seam. Don't plan around them
|
|
221
238
|
shipping.
|
|
239
|
+
- **`onboardingInvite` is a template with no built-in caller.** It is seeded into
|
|
240
|
+
the group's email settings (so you can edit its copy) but chapter never fires
|
|
241
|
+
it, because the moment it would fire — approval — is a host route (approval is
|
|
242
|
+
out of scope, above). Send it from your own approve handler via `sendTemplated`;
|
|
243
|
+
the seed existing does not mean the built-ins send it.
|
|
222
244
|
|
|
223
245
|
### Verify from the types, not this file
|
|
224
246
|
|
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
canTransition: () => canTransition,
|
|
34
34
|
canceledPatch: () => canceledPatch,
|
|
35
35
|
chapterDb: () => chapterDb,
|
|
36
|
+
clampArray: () => clampArray,
|
|
36
37
|
clerkInviteRequest: () => clerkInviteRequest,
|
|
37
38
|
clerkUserRequest: () => clerkUserRequest,
|
|
38
39
|
createChapterIntegration: () => createChapterIntegration,
|
|
@@ -51,6 +52,7 @@ __export(index_exports, {
|
|
|
51
52
|
isAlreadySent: () => isAlreadySent,
|
|
52
53
|
isReconcilable: () => isReconcilable,
|
|
53
54
|
isSlotAvailable: () => isSlotAvailable,
|
|
55
|
+
isValidEmail: () => isValidEmail,
|
|
54
56
|
joinConfig: () => joinConfig,
|
|
55
57
|
meetingCreateRow: () => meetingCreateRow,
|
|
56
58
|
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
@@ -482,27 +484,47 @@ function resolveApplication(a) {
|
|
|
482
484
|
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
483
485
|
}
|
|
484
486
|
}
|
|
487
|
+
if (a?.profileFields !== void 0 && (!Array.isArray(a.profileFields) || !a.profileFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
488
|
+
throw new Error("defineChapter.application.profileFields: must be an array of field-name strings");
|
|
489
|
+
}
|
|
490
|
+
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
491
|
+
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
492
|
+
}
|
|
485
493
|
return {
|
|
486
494
|
required,
|
|
487
495
|
optional,
|
|
488
496
|
maxLen: a?.maxLen ?? {},
|
|
489
497
|
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
490
498
|
bodyCap: a?.bodyCap ?? 32768,
|
|
491
|
-
requireDisclaimerAck: a?.requireDisclaimerAck ?? false
|
|
499
|
+
requireDisclaimerAck: a?.requireDisclaimerAck ?? false,
|
|
500
|
+
profileFields: a?.profileFields ?? null,
|
|
501
|
+
crmFields: a?.crmFields ?? [],
|
|
502
|
+
maxArrayLen: a?.maxArrayLen ?? 100,
|
|
503
|
+
validateEmail: a?.validateEmail ?? true
|
|
492
504
|
};
|
|
493
505
|
}
|
|
506
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
507
|
+
function isValidEmail(value) {
|
|
508
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
509
|
+
}
|
|
510
|
+
function clampArray(value, max) {
|
|
511
|
+
if (!Array.isArray(value)) return value;
|
|
512
|
+
return value.filter((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean").slice(0, max);
|
|
513
|
+
}
|
|
494
514
|
function hasDisclaimerAck(fields) {
|
|
495
515
|
return fields.disclaimerAck === true || fields.disclaimerAck === "true";
|
|
496
516
|
}
|
|
497
517
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
498
518
|
function applicantProfile(chapter, fields) {
|
|
519
|
+
const app = chapter.application;
|
|
520
|
+
const allowed = (f) => app.profileFields === null || app.profileFields.includes(f);
|
|
499
521
|
const profile = {};
|
|
500
|
-
for (const f of [...
|
|
501
|
-
if (IDENTITY_FIELDS.has(f)) continue;
|
|
522
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
523
|
+
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
502
524
|
const v = fields[f];
|
|
503
525
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
504
526
|
}
|
|
505
|
-
if (fields.focus !== void 0) profile.focus = fields.focus;
|
|
527
|
+
if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
506
528
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
507
529
|
}
|
|
508
530
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -516,6 +538,9 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
516
538
|
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
517
539
|
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
518
540
|
}
|
|
541
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
542
|
+
return { ok: false, error: "email must be a valid email address" };
|
|
543
|
+
}
|
|
519
544
|
const acked = hasDisclaimerAck(fields);
|
|
520
545
|
if (app.requireDisclaimerAck && !acked) {
|
|
521
546
|
return { ok: false, error: "disclaimerAck is required" };
|
|
@@ -525,7 +550,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
525
550
|
for (const f of [...app.required, ...app.optional]) {
|
|
526
551
|
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
527
552
|
}
|
|
528
|
-
if (fields.focus !== void 0) row.focus = fields.focus;
|
|
553
|
+
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
529
554
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
530
555
|
if (acked) row.disclaimerAckAt = opts.now;
|
|
531
556
|
const { duplicate } = await db.transact(
|
|
@@ -867,7 +892,7 @@ async function projectSharedRecord(deps, person) {
|
|
|
867
892
|
return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });
|
|
868
893
|
}
|
|
869
894
|
async function projectApplicant(deps, applicant) {
|
|
870
|
-
const
|
|
895
|
+
const base = sharedPersonInput({
|
|
871
896
|
email: applicant.email,
|
|
872
897
|
firstName: applicant.firstName,
|
|
873
898
|
lastName: applicant.lastName,
|
|
@@ -875,7 +900,14 @@ async function projectApplicant(deps, applicant) {
|
|
|
875
900
|
linkedin: applicant.linkedin,
|
|
876
901
|
hubRecordId: applicant.applicationId
|
|
877
902
|
});
|
|
878
|
-
|
|
903
|
+
const mutationId = `apply:${applicant.applicationId}`;
|
|
904
|
+
const extra = applicant.extra ?? {};
|
|
905
|
+
if (Object.keys(extra).length === 0) return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
906
|
+
try {
|
|
907
|
+
return await upsertPerson(deps, { email: applicant.email, input: { ...base, ...extra }, mutationId });
|
|
908
|
+
} catch {
|
|
909
|
+
return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
|
|
910
|
+
}
|
|
879
911
|
}
|
|
880
912
|
|
|
881
913
|
// src/clerk.ts
|