@odla-ai/chapter 0.31.3 → 0.31.4
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 +8 -0
- package/dist/{copy-context-HAhNTZdl.d.ts → copy-context-BXBEdp4p.d.ts} +3 -3
- package/dist/index.cjs +192 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +192 -142
- 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.map +1 -1
- package/dist/worker/index.d.cts +3 -3
- package/dist/worker/index.d.ts +3 -3
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -481,9 +481,9 @@ interface ResolvedPipeline {
|
|
|
481
481
|
}
|
|
482
482
|
/** The application (join form) validation surface — which string fields are
|
|
483
483
|
* required vs accepted, their max lengths, and the request body cap. Drives
|
|
484
|
-
* submit validation
|
|
485
|
-
*
|
|
486
|
-
*
|
|
484
|
+
* submit validation, the generated `applications` schema, and the CRM slot
|
|
485
|
+
* projection; defaults to the reference form. Built-in fields retain their
|
|
486
|
+
* declared schema types/indexes, and site-defined fields become string attrs. */
|
|
487
487
|
interface ChapterApplication {
|
|
488
488
|
required?: readonly string[];
|
|
489
489
|
optional?: readonly string[];
|
|
@@ -761,6 +761,11 @@ interface ChapterRunbookHints {
|
|
|
761
761
|
declare function createChapterIntegration(chapter: Chapter, options?: ChapterIntegrationOptions): ChapterIntegrationDescriptor;
|
|
762
762
|
|
|
763
763
|
/** The chapter's own schema + deny-all rules for a mode + auth policy.
|
|
764
|
+
*
|
|
765
|
+
* In chapter mode, the optional {@link ResolvedApplication} supplies the exact
|
|
766
|
+
* required/optional application string attrs. Omit it for the reference form.
|
|
767
|
+
* Built-in attrs retain their types/indexes; site-defined fields become string
|
|
768
|
+
* attrs, while package-owned operational attrs cannot be repurposed as inputs.
|
|
764
769
|
*
|
|
765
770
|
* Operational tables (`applications`/`groups`/`meetings`/`emailLog`) are added in
|
|
766
771
|
* `chapter` mode only. The auth tables follow {@link ResolvedAuth}: `source:
|
|
@@ -768,7 +773,7 @@ declare function createChapterIntegration(chapter: Chapter, options?: ChapterInt
|
|
|
768
773
|
* read-only super-admin tier (default on for the `"claim"` ladder). A `"claim"`
|
|
769
774
|
* chapter therefore emits the reference namespace set — `applications`,
|
|
770
775
|
* `groups`, `meetings`, `emailLog`, `superAdmins` — with no `admins` table. */
|
|
771
|
-
declare function chapterDb(mode: ChapterMode, auth: ResolvedAuth, includeNetworkNotes?: boolean): {
|
|
776
|
+
declare function chapterDb(mode: ChapterMode, auth: ResolvedAuth, includeNetworkNotes?: boolean, application?: ResolvedApplication): {
|
|
772
777
|
schema: DbSchema;
|
|
773
778
|
rules: DbRules;
|
|
774
779
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -481,9 +481,9 @@ interface ResolvedPipeline {
|
|
|
481
481
|
}
|
|
482
482
|
/** The application (join form) validation surface — which string fields are
|
|
483
483
|
* required vs accepted, their max lengths, and the request body cap. Drives
|
|
484
|
-
* submit validation
|
|
485
|
-
*
|
|
486
|
-
*
|
|
484
|
+
* submit validation, the generated `applications` schema, and the CRM slot
|
|
485
|
+
* projection; defaults to the reference form. Built-in fields retain their
|
|
486
|
+
* declared schema types/indexes, and site-defined fields become string attrs. */
|
|
487
487
|
interface ChapterApplication {
|
|
488
488
|
required?: readonly string[];
|
|
489
489
|
optional?: readonly string[];
|
|
@@ -761,6 +761,11 @@ interface ChapterRunbookHints {
|
|
|
761
761
|
declare function createChapterIntegration(chapter: Chapter, options?: ChapterIntegrationOptions): ChapterIntegrationDescriptor;
|
|
762
762
|
|
|
763
763
|
/** The chapter's own schema + deny-all rules for a mode + auth policy.
|
|
764
|
+
*
|
|
765
|
+
* In chapter mode, the optional {@link ResolvedApplication} supplies the exact
|
|
766
|
+
* required/optional application string attrs. Omit it for the reference form.
|
|
767
|
+
* Built-in attrs retain their types/indexes; site-defined fields become string
|
|
768
|
+
* attrs, while package-owned operational attrs cannot be repurposed as inputs.
|
|
764
769
|
*
|
|
765
770
|
* Operational tables (`applications`/`groups`/`meetings`/`emailLog`) are added in
|
|
766
771
|
* `chapter` mode only. The auth tables follow {@link ResolvedAuth}: `source:
|
|
@@ -768,7 +773,7 @@ declare function createChapterIntegration(chapter: Chapter, options?: ChapterInt
|
|
|
768
773
|
* read-only super-admin tier (default on for the `"claim"` ladder). A `"claim"`
|
|
769
774
|
* chapter therefore emits the reference namespace set — `applications`,
|
|
770
775
|
* `groups`, `meetings`, `emailLog`, `superAdmins` — with no `admins` table. */
|
|
771
|
-
declare function chapterDb(mode: ChapterMode, auth: ResolvedAuth, includeNetworkNotes?: boolean): {
|
|
776
|
+
declare function chapterDb(mode: ChapterMode, auth: ResolvedAuth, includeNetworkNotes?: boolean, application?: ResolvedApplication): {
|
|
772
777
|
schema: DbSchema;
|
|
773
778
|
rules: DbRules;
|
|
774
779
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,154 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
import { defineCrm } from "@odla-ai/crm";
|
|
3
3
|
|
|
4
|
+
// src/member.ts
|
|
5
|
+
import { assertFieldCondition, resolveFieldStates } from "@odla-ai/crm";
|
|
6
|
+
|
|
7
|
+
// src/application-id.ts
|
|
8
|
+
var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
|
|
9
|
+
async function applicationIdForSubmission(submissionId) {
|
|
10
|
+
const input = new TextEncoder().encode(`${APPLICATION_ID_DOMAIN}${submissionId}`);
|
|
11
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", input)).slice(0, 16);
|
|
12
|
+
bytes[6] = bytes[6] & 15 | 128;
|
|
13
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
14
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/member.ts
|
|
19
|
+
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
20
|
+
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
21
|
+
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
22
|
+
function resolveApplication(a) {
|
|
23
|
+
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
24
|
+
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
25
|
+
for (const [name, arr] of [["required", required], ["optional", optional]]) {
|
|
26
|
+
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
27
|
+
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
28
|
+
}
|
|
29
|
+
const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
|
|
30
|
+
if (unsafe) {
|
|
31
|
+
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const duplicate = [...required, ...optional].find((field, index, fields) => fields.indexOf(field) !== index);
|
|
35
|
+
if (duplicate) {
|
|
36
|
+
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
37
|
+
}
|
|
38
|
+
if (a?.profileFields !== void 0 && (!Array.isArray(a.profileFields) || !a.profileFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
39
|
+
throw new Error("defineChapter.application.profileFields: must be an array of field-name strings");
|
|
40
|
+
}
|
|
41
|
+
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
42
|
+
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
43
|
+
}
|
|
44
|
+
const conditions = a?.conditions ?? {};
|
|
45
|
+
for (const [field, declared] of Object.entries(conditions)) {
|
|
46
|
+
for (const key of ["visibleWhen", "requiredWhen"]) {
|
|
47
|
+
const expression = declared?.[key];
|
|
48
|
+
if (expression === void 0) continue;
|
|
49
|
+
if (typeof expression !== "string" || !expression.trim()) {
|
|
50
|
+
throw new Error(`defineChapter.application.conditions.${field}.${key}: must be a non-empty CEL condition`);
|
|
51
|
+
}
|
|
52
|
+
assertFieldCondition(expression, `defineChapter.application.conditions.${field}.${key}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
required,
|
|
57
|
+
optional,
|
|
58
|
+
conditions,
|
|
59
|
+
maxLen: a?.maxLen ?? {},
|
|
60
|
+
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
61
|
+
bodyCap: a?.bodyCap ?? 32768,
|
|
62
|
+
requireDisclaimerAck: a?.requireDisclaimerAck ?? true,
|
|
63
|
+
profileFields: a?.profileFields ?? [],
|
|
64
|
+
crmFields: a?.crmFields ?? [],
|
|
65
|
+
maxArrayLen: a?.maxArrayLen ?? 100,
|
|
66
|
+
validateEmail: a?.validateEmail ?? true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
70
|
+
function isValidEmail(value) {
|
|
71
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
72
|
+
}
|
|
73
|
+
function clampArray(value, max) {
|
|
74
|
+
if (!Array.isArray(value)) return value;
|
|
75
|
+
return value.filter((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean").slice(0, max);
|
|
76
|
+
}
|
|
77
|
+
function hasDisclaimerAck(fields) {
|
|
78
|
+
return fields.disclaimerAck === true || fields.disclaimerAck === "true";
|
|
79
|
+
}
|
|
80
|
+
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
81
|
+
function applicantProfile(chapter, fields) {
|
|
82
|
+
const app = chapter.application;
|
|
83
|
+
const allowed = (f) => app.profileFields.includes(f);
|
|
84
|
+
const profile = {};
|
|
85
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
86
|
+
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
87
|
+
const v = fields[f];
|
|
88
|
+
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
89
|
+
}
|
|
90
|
+
if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
91
|
+
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
92
|
+
}
|
|
93
|
+
async function submitApplication(db, chapter, fields, opts) {
|
|
94
|
+
const app = chapter.application;
|
|
95
|
+
const fieldStates = resolveFieldStates(app.conditions ?? {}, fields, app.required);
|
|
96
|
+
for (const [f, state] of Object.entries(fieldStates)) {
|
|
97
|
+
if (!state.required) continue;
|
|
98
|
+
const v = fields[f];
|
|
99
|
+
if (typeof v !== "string" || v.trim() === "") return { ok: false, error: `${f} is required` };
|
|
100
|
+
}
|
|
101
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
102
|
+
const v = fields[f];
|
|
103
|
+
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
104
|
+
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
105
|
+
}
|
|
106
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
107
|
+
return { ok: false, error: "email must be a valid email address" };
|
|
108
|
+
}
|
|
109
|
+
const acked = hasDisclaimerAck(fields);
|
|
110
|
+
if (app.requireDisclaimerAck && !acked) {
|
|
111
|
+
return { ok: false, error: "disclaimerAck is required" };
|
|
112
|
+
}
|
|
113
|
+
const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
114
|
+
const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
115
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
116
|
+
if (fieldStates[f]?.visible === false) continue;
|
|
117
|
+
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
118
|
+
}
|
|
119
|
+
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
120
|
+
if (opts.groupId) row.groupId = opts.groupId;
|
|
121
|
+
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
122
|
+
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
123
|
+
return { ok: false, error: "tierId is not an offered tier" };
|
|
124
|
+
}
|
|
125
|
+
row.tierId = fields.tierId;
|
|
126
|
+
}
|
|
127
|
+
if (acked) row.disclaimerAckAt = opts.now;
|
|
128
|
+
const { duplicate } = await db.transact(
|
|
129
|
+
[{ t: "update", ns: "applications", id: id2, attrs: row }],
|
|
130
|
+
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
131
|
+
);
|
|
132
|
+
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
133
|
+
}
|
|
134
|
+
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
135
|
+
return {
|
|
136
|
+
id: group.id,
|
|
137
|
+
name: group.name,
|
|
138
|
+
standardPriceCents: group.standardPriceCents ?? 0,
|
|
139
|
+
foundingDiscountCents: group.foundingDiscountCents ?? 0,
|
|
140
|
+
tiers: [...tiers2],
|
|
141
|
+
// The browser evaluates the same conditions the server enforces.
|
|
142
|
+
conditions,
|
|
143
|
+
disclaimerText: group.disclaimerText ?? "",
|
|
144
|
+
refundPolicyText: group.refundPolicyText ?? "",
|
|
145
|
+
trustCopy: group.trustCopy ?? "",
|
|
146
|
+
commitmentText: group.commitmentText ?? "",
|
|
147
|
+
normsText: group.normsText ?? "",
|
|
148
|
+
paymentsReady: paymentsReady2
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
4
152
|
// src/schema.ts
|
|
5
153
|
function attr(type, flags = {}) {
|
|
6
154
|
return {
|
|
@@ -75,6 +223,47 @@ var applications = {
|
|
|
75
223
|
canceled: attr("boolean", { optional: true })
|
|
76
224
|
}
|
|
77
225
|
};
|
|
226
|
+
var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
227
|
+
"firstName",
|
|
228
|
+
"lastName",
|
|
229
|
+
"email",
|
|
230
|
+
"referral",
|
|
231
|
+
"referralName",
|
|
232
|
+
"whoYouAre",
|
|
233
|
+
"focus",
|
|
234
|
+
"linkedin",
|
|
235
|
+
"message",
|
|
236
|
+
"phone",
|
|
237
|
+
"state"
|
|
238
|
+
]);
|
|
239
|
+
function applicationsFor(application) {
|
|
240
|
+
const attrs = {};
|
|
241
|
+
for (const [name, spec] of Object.entries(applications.attrs)) {
|
|
242
|
+
attrs[name] = {
|
|
243
|
+
...spec,
|
|
244
|
+
optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const apply = (field, optional) => {
|
|
248
|
+
const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
|
|
249
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
|
|
250
|
+
throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
|
|
251
|
+
}
|
|
252
|
+
if (existing && existing.type !== "string") {
|
|
253
|
+
throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
254
|
+
}
|
|
255
|
+
const spec = existing ? { ...existing, optional } : attr("string", { optional });
|
|
256
|
+
Object.defineProperty(attrs, field, {
|
|
257
|
+
value: spec,
|
|
258
|
+
enumerable: true,
|
|
259
|
+
configurable: true,
|
|
260
|
+
writable: true
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
for (const field of application.required) apply(field, false);
|
|
264
|
+
for (const field of application.optional) apply(field, true);
|
|
265
|
+
return { attrs };
|
|
266
|
+
}
|
|
78
267
|
var groups = {
|
|
79
268
|
attrs: {
|
|
80
269
|
id: id(),
|
|
@@ -146,10 +335,10 @@ var emailLog = {
|
|
|
146
335
|
sentAt: attr("number", { indexed: true })
|
|
147
336
|
}
|
|
148
337
|
};
|
|
149
|
-
function chapterDb(mode, auth, includeNetworkNotes = false) {
|
|
338
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0)) {
|
|
150
339
|
const entities = {};
|
|
151
340
|
if (mode === "chapter") {
|
|
152
|
-
entities.applications =
|
|
341
|
+
entities.applications = applicationsFor(application);
|
|
153
342
|
entities.groups = groups;
|
|
154
343
|
entities.tiers = tiers;
|
|
155
344
|
entities.meetings = meetings;
|
|
@@ -412,145 +601,6 @@ function canApprove(status, p) {
|
|
|
412
601
|
return p.approvableFrom.includes(status);
|
|
413
602
|
}
|
|
414
603
|
|
|
415
|
-
// src/member.ts
|
|
416
|
-
import { assertFieldCondition, resolveFieldStates } from "@odla-ai/crm";
|
|
417
|
-
|
|
418
|
-
// src/application-id.ts
|
|
419
|
-
var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
|
|
420
|
-
async function applicationIdForSubmission(submissionId) {
|
|
421
|
-
const input = new TextEncoder().encode(`${APPLICATION_ID_DOMAIN}${submissionId}`);
|
|
422
|
-
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", input)).slice(0, 16);
|
|
423
|
-
bytes[6] = bytes[6] & 15 | 128;
|
|
424
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
425
|
-
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
426
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// src/member.ts
|
|
430
|
-
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
431
|
-
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
432
|
-
function resolveApplication(a) {
|
|
433
|
-
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
434
|
-
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
435
|
-
for (const [name, arr] of [["required", required], ["optional", optional]]) {
|
|
436
|
-
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
437
|
-
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
if (a?.profileFields !== void 0 && (!Array.isArray(a.profileFields) || !a.profileFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
441
|
-
throw new Error("defineChapter.application.profileFields: must be an array of field-name strings");
|
|
442
|
-
}
|
|
443
|
-
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
444
|
-
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
445
|
-
}
|
|
446
|
-
const conditions = a?.conditions ?? {};
|
|
447
|
-
for (const [field, declared] of Object.entries(conditions)) {
|
|
448
|
-
for (const key of ["visibleWhen", "requiredWhen"]) {
|
|
449
|
-
const expression = declared?.[key];
|
|
450
|
-
if (expression === void 0) continue;
|
|
451
|
-
if (typeof expression !== "string" || !expression.trim()) {
|
|
452
|
-
throw new Error(`defineChapter.application.conditions.${field}.${key}: must be a non-empty CEL condition`);
|
|
453
|
-
}
|
|
454
|
-
assertFieldCondition(expression, `defineChapter.application.conditions.${field}.${key}`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return {
|
|
458
|
-
required,
|
|
459
|
-
optional,
|
|
460
|
-
conditions,
|
|
461
|
-
maxLen: a?.maxLen ?? {},
|
|
462
|
-
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
463
|
-
bodyCap: a?.bodyCap ?? 32768,
|
|
464
|
-
requireDisclaimerAck: a?.requireDisclaimerAck ?? true,
|
|
465
|
-
profileFields: a?.profileFields ?? [],
|
|
466
|
-
crmFields: a?.crmFields ?? [],
|
|
467
|
-
maxArrayLen: a?.maxArrayLen ?? 100,
|
|
468
|
-
validateEmail: a?.validateEmail ?? true
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
472
|
-
function isValidEmail(value) {
|
|
473
|
-
return typeof value === "string" && EMAIL_RE.test(value);
|
|
474
|
-
}
|
|
475
|
-
function clampArray(value, max) {
|
|
476
|
-
if (!Array.isArray(value)) return value;
|
|
477
|
-
return value.filter((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean").slice(0, max);
|
|
478
|
-
}
|
|
479
|
-
function hasDisclaimerAck(fields) {
|
|
480
|
-
return fields.disclaimerAck === true || fields.disclaimerAck === "true";
|
|
481
|
-
}
|
|
482
|
-
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
483
|
-
function applicantProfile(chapter, fields) {
|
|
484
|
-
const app = chapter.application;
|
|
485
|
-
const allowed = (f) => app.profileFields.includes(f);
|
|
486
|
-
const profile = {};
|
|
487
|
-
for (const f of [...app.required, ...app.optional]) {
|
|
488
|
-
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
489
|
-
const v = fields[f];
|
|
490
|
-
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
491
|
-
}
|
|
492
|
-
if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
493
|
-
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
494
|
-
}
|
|
495
|
-
async function submitApplication(db, chapter, fields, opts) {
|
|
496
|
-
const app = chapter.application;
|
|
497
|
-
const fieldStates = resolveFieldStates(app.conditions ?? {}, fields, app.required);
|
|
498
|
-
for (const [f, state] of Object.entries(fieldStates)) {
|
|
499
|
-
if (!state.required) continue;
|
|
500
|
-
const v = fields[f];
|
|
501
|
-
if (typeof v !== "string" || v.trim() === "") return { ok: false, error: `${f} is required` };
|
|
502
|
-
}
|
|
503
|
-
for (const f of [...app.required, ...app.optional]) {
|
|
504
|
-
const v = fields[f];
|
|
505
|
-
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
506
|
-
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
507
|
-
}
|
|
508
|
-
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
509
|
-
return { ok: false, error: "email must be a valid email address" };
|
|
510
|
-
}
|
|
511
|
-
const acked = hasDisclaimerAck(fields);
|
|
512
|
-
if (app.requireDisclaimerAck && !acked) {
|
|
513
|
-
return { ok: false, error: "disclaimerAck is required" };
|
|
514
|
-
}
|
|
515
|
-
const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
516
|
-
const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
517
|
-
for (const f of [...app.required, ...app.optional]) {
|
|
518
|
-
if (fieldStates[f]?.visible === false) continue;
|
|
519
|
-
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
520
|
-
}
|
|
521
|
-
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
522
|
-
if (opts.groupId) row.groupId = opts.groupId;
|
|
523
|
-
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
524
|
-
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
525
|
-
return { ok: false, error: "tierId is not an offered tier" };
|
|
526
|
-
}
|
|
527
|
-
row.tierId = fields.tierId;
|
|
528
|
-
}
|
|
529
|
-
if (acked) row.disclaimerAckAt = opts.now;
|
|
530
|
-
const { duplicate } = await db.transact(
|
|
531
|
-
[{ t: "update", ns: "applications", id: id2, attrs: row }],
|
|
532
|
-
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
533
|
-
);
|
|
534
|
-
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
535
|
-
}
|
|
536
|
-
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
537
|
-
return {
|
|
538
|
-
id: group.id,
|
|
539
|
-
name: group.name,
|
|
540
|
-
standardPriceCents: group.standardPriceCents ?? 0,
|
|
541
|
-
foundingDiscountCents: group.foundingDiscountCents ?? 0,
|
|
542
|
-
tiers: [...tiers2],
|
|
543
|
-
// The browser evaluates the same conditions the server enforces.
|
|
544
|
-
conditions,
|
|
545
|
-
disclaimerText: group.disclaimerText ?? "",
|
|
546
|
-
refundPolicyText: group.refundPolicyText ?? "",
|
|
547
|
-
trustCopy: group.trustCopy ?? "",
|
|
548
|
-
commitmentText: group.commitmentText ?? "",
|
|
549
|
-
normsText: group.normsText ?? "",
|
|
550
|
-
paymentsReady: paymentsReady2
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
|
|
554
604
|
// src/copy-defaults-admin.ts
|
|
555
605
|
var DEFAULT_ADMIN_COPY = {
|
|
556
606
|
auth: {
|
|
@@ -1099,7 +1149,7 @@ function defineChapter(config) {
|
|
|
1099
1149
|
const copy = resolveChapterCopy(config.copy);
|
|
1100
1150
|
const network = resolveNetwork(config, crm);
|
|
1101
1151
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1102
|
-
const { schema, rules } = chapterDb(mode, auth, network.targets.length > 0);
|
|
1152
|
+
const { schema, rules } = chapterDb(mode, auth, network.targets.length > 0, application);
|
|
1103
1153
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1104
1154
|
const account = config.account ?? "none";
|
|
1105
1155
|
if (account !== "invite" && account !== "create" && account !== "none") {
|