@odla-ai/chapter 0.31.3 → 0.32.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 +100 -2
- package/dist/{chunk-QWMP6TSM.js → chunk-BBCT7U3D.js} +282 -2
- package/dist/chunk-BBCT7U3D.js.map +1 -0
- package/dist/{copy-context-HAhNTZdl.d.ts → copy-context-BXBEdp4p.d.ts} +3 -3
- package/dist/index.cjs +363 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -5
- package/dist/index.d.ts +79 -5
- package/dist/index.js +363 -142
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -1
- package/dist/ui/member/index.d.ts +74 -3
- package/dist/ui/member/index.js +5 -1
- 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/chunk-QWMP6TSM.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
|
|
24
24
|
DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
|
|
25
|
+
MEMBERSHIP_PAGE_RENDERER: () => MEMBERSHIP_PAGE_RENDERER,
|
|
25
26
|
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
26
27
|
applicantProfile: () => applicantProfile,
|
|
27
28
|
applicationBookingUpdate: () => applicationBookingUpdate,
|
|
@@ -77,6 +78,7 @@ __export(index_exports, {
|
|
|
77
78
|
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
78
79
|
memberApplication: () => memberApplication,
|
|
79
80
|
memberSession: () => memberSession,
|
|
81
|
+
membershipPageDocument: () => membershipPageDocument,
|
|
80
82
|
networkSourceTag: () => networkSourceTag,
|
|
81
83
|
normalizeSharedRecord: () => normalizeSharedRecord,
|
|
82
84
|
normalizeWebhookEvent: () => normalizeWebhookEvent,
|
|
@@ -115,6 +117,7 @@ __export(index_exports, {
|
|
|
115
117
|
tierPayable: () => tierPayable,
|
|
116
118
|
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
117
119
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
120
|
+
validateMembershipPageDocument: () => validateMembershipPageDocument,
|
|
118
121
|
validateScheduling: () => validateScheduling,
|
|
119
122
|
verifyStripeSignature: () => import_stripe.verifyStripeSignature,
|
|
120
123
|
webhookMutationId: () => webhookMutationId
|
|
@@ -124,6 +127,154 @@ module.exports = __toCommonJS(index_exports);
|
|
|
124
127
|
// src/config.ts
|
|
125
128
|
var import_crm2 = require("@odla-ai/crm");
|
|
126
129
|
|
|
130
|
+
// src/member.ts
|
|
131
|
+
var import_crm = require("@odla-ai/crm");
|
|
132
|
+
|
|
133
|
+
// src/application-id.ts
|
|
134
|
+
var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
|
|
135
|
+
async function applicationIdForSubmission(submissionId) {
|
|
136
|
+
const input = new TextEncoder().encode(`${APPLICATION_ID_DOMAIN}${submissionId}`);
|
|
137
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", input)).slice(0, 16);
|
|
138
|
+
bytes[6] = bytes[6] & 15 | 128;
|
|
139
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
140
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
141
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/member.ts
|
|
145
|
+
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
146
|
+
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
147
|
+
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
148
|
+
function resolveApplication(a) {
|
|
149
|
+
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
150
|
+
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
151
|
+
for (const [name, arr] of [["required", required], ["optional", optional]]) {
|
|
152
|
+
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
153
|
+
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
154
|
+
}
|
|
155
|
+
const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
|
|
156
|
+
if (unsafe) {
|
|
157
|
+
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const duplicate = [...required, ...optional].find((field, index, fields) => fields.indexOf(field) !== index);
|
|
161
|
+
if (duplicate) {
|
|
162
|
+
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
163
|
+
}
|
|
164
|
+
if (a?.profileFields !== void 0 && (!Array.isArray(a.profileFields) || !a.profileFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
165
|
+
throw new Error("defineChapter.application.profileFields: must be an array of field-name strings");
|
|
166
|
+
}
|
|
167
|
+
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
168
|
+
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
169
|
+
}
|
|
170
|
+
const conditions = a?.conditions ?? {};
|
|
171
|
+
for (const [field, declared] of Object.entries(conditions)) {
|
|
172
|
+
for (const key of ["visibleWhen", "requiredWhen"]) {
|
|
173
|
+
const expression = declared?.[key];
|
|
174
|
+
if (expression === void 0) continue;
|
|
175
|
+
if (typeof expression !== "string" || !expression.trim()) {
|
|
176
|
+
throw new Error(`defineChapter.application.conditions.${field}.${key}: must be a non-empty CEL condition`);
|
|
177
|
+
}
|
|
178
|
+
(0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
required,
|
|
183
|
+
optional,
|
|
184
|
+
conditions,
|
|
185
|
+
maxLen: a?.maxLen ?? {},
|
|
186
|
+
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
187
|
+
bodyCap: a?.bodyCap ?? 32768,
|
|
188
|
+
requireDisclaimerAck: a?.requireDisclaimerAck ?? true,
|
|
189
|
+
profileFields: a?.profileFields ?? [],
|
|
190
|
+
crmFields: a?.crmFields ?? [],
|
|
191
|
+
maxArrayLen: a?.maxArrayLen ?? 100,
|
|
192
|
+
validateEmail: a?.validateEmail ?? true
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
196
|
+
function isValidEmail(value) {
|
|
197
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
198
|
+
}
|
|
199
|
+
function clampArray(value, max) {
|
|
200
|
+
if (!Array.isArray(value)) return value;
|
|
201
|
+
return value.filter((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean").slice(0, max);
|
|
202
|
+
}
|
|
203
|
+
function hasDisclaimerAck(fields) {
|
|
204
|
+
return fields.disclaimerAck === true || fields.disclaimerAck === "true";
|
|
205
|
+
}
|
|
206
|
+
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
207
|
+
function applicantProfile(chapter, fields) {
|
|
208
|
+
const app = chapter.application;
|
|
209
|
+
const allowed = (f) => app.profileFields.includes(f);
|
|
210
|
+
const profile = {};
|
|
211
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
212
|
+
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
213
|
+
const v = fields[f];
|
|
214
|
+
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
215
|
+
}
|
|
216
|
+
if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
217
|
+
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
218
|
+
}
|
|
219
|
+
async function submitApplication(db, chapter, fields, opts) {
|
|
220
|
+
const app = chapter.application;
|
|
221
|
+
const fieldStates = (0, import_crm.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
|
|
222
|
+
for (const [f, state] of Object.entries(fieldStates)) {
|
|
223
|
+
if (!state.required) continue;
|
|
224
|
+
const v = fields[f];
|
|
225
|
+
if (typeof v !== "string" || v.trim() === "") return { ok: false, error: `${f} is required` };
|
|
226
|
+
}
|
|
227
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
228
|
+
const v = fields[f];
|
|
229
|
+
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
230
|
+
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
231
|
+
}
|
|
232
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
233
|
+
return { ok: false, error: "email must be a valid email address" };
|
|
234
|
+
}
|
|
235
|
+
const acked = hasDisclaimerAck(fields);
|
|
236
|
+
if (app.requireDisclaimerAck && !acked) {
|
|
237
|
+
return { ok: false, error: "disclaimerAck is required" };
|
|
238
|
+
}
|
|
239
|
+
const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
240
|
+
const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
241
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
242
|
+
if (fieldStates[f]?.visible === false) continue;
|
|
243
|
+
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
244
|
+
}
|
|
245
|
+
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
246
|
+
if (opts.groupId) row.groupId = opts.groupId;
|
|
247
|
+
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
248
|
+
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
249
|
+
return { ok: false, error: "tierId is not an offered tier" };
|
|
250
|
+
}
|
|
251
|
+
row.tierId = fields.tierId;
|
|
252
|
+
}
|
|
253
|
+
if (acked) row.disclaimerAckAt = opts.now;
|
|
254
|
+
const { duplicate } = await db.transact(
|
|
255
|
+
[{ t: "update", ns: "applications", id: id2, attrs: row }],
|
|
256
|
+
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
257
|
+
);
|
|
258
|
+
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
259
|
+
}
|
|
260
|
+
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
261
|
+
return {
|
|
262
|
+
id: group.id,
|
|
263
|
+
name: group.name,
|
|
264
|
+
standardPriceCents: group.standardPriceCents ?? 0,
|
|
265
|
+
foundingDiscountCents: group.foundingDiscountCents ?? 0,
|
|
266
|
+
tiers: [...tiers2],
|
|
267
|
+
// The browser evaluates the same conditions the server enforces.
|
|
268
|
+
conditions,
|
|
269
|
+
disclaimerText: group.disclaimerText ?? "",
|
|
270
|
+
refundPolicyText: group.refundPolicyText ?? "",
|
|
271
|
+
trustCopy: group.trustCopy ?? "",
|
|
272
|
+
commitmentText: group.commitmentText ?? "",
|
|
273
|
+
normsText: group.normsText ?? "",
|
|
274
|
+
paymentsReady: paymentsReady2
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
127
278
|
// src/schema.ts
|
|
128
279
|
function attr(type, flags = {}) {
|
|
129
280
|
return {
|
|
@@ -198,6 +349,47 @@ var applications = {
|
|
|
198
349
|
canceled: attr("boolean", { optional: true })
|
|
199
350
|
}
|
|
200
351
|
};
|
|
352
|
+
var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
353
|
+
"firstName",
|
|
354
|
+
"lastName",
|
|
355
|
+
"email",
|
|
356
|
+
"referral",
|
|
357
|
+
"referralName",
|
|
358
|
+
"whoYouAre",
|
|
359
|
+
"focus",
|
|
360
|
+
"linkedin",
|
|
361
|
+
"message",
|
|
362
|
+
"phone",
|
|
363
|
+
"state"
|
|
364
|
+
]);
|
|
365
|
+
function applicationsFor(application) {
|
|
366
|
+
const attrs = {};
|
|
367
|
+
for (const [name, spec] of Object.entries(applications.attrs)) {
|
|
368
|
+
attrs[name] = {
|
|
369
|
+
...spec,
|
|
370
|
+
optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const apply = (field, optional) => {
|
|
374
|
+
const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
|
|
375
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
|
|
376
|
+
throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
|
|
377
|
+
}
|
|
378
|
+
if (existing && existing.type !== "string") {
|
|
379
|
+
throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
380
|
+
}
|
|
381
|
+
const spec = existing ? { ...existing, optional } : attr("string", { optional });
|
|
382
|
+
Object.defineProperty(attrs, field, {
|
|
383
|
+
value: spec,
|
|
384
|
+
enumerable: true,
|
|
385
|
+
configurable: true,
|
|
386
|
+
writable: true
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
for (const field of application.required) apply(field, false);
|
|
390
|
+
for (const field of application.optional) apply(field, true);
|
|
391
|
+
return { attrs };
|
|
392
|
+
}
|
|
201
393
|
var groups = {
|
|
202
394
|
attrs: {
|
|
203
395
|
id: id(),
|
|
@@ -269,10 +461,10 @@ var emailLog = {
|
|
|
269
461
|
sentAt: attr("number", { indexed: true })
|
|
270
462
|
}
|
|
271
463
|
};
|
|
272
|
-
function chapterDb(mode, auth, includeNetworkNotes = false) {
|
|
464
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0)) {
|
|
273
465
|
const entities = {};
|
|
274
466
|
if (mode === "chapter") {
|
|
275
|
-
entities.applications =
|
|
467
|
+
entities.applications = applicationsFor(application);
|
|
276
468
|
entities.groups = groups;
|
|
277
469
|
entities.tiers = tiers;
|
|
278
470
|
entities.meetings = meetings;
|
|
@@ -535,145 +727,6 @@ function canApprove(status, p) {
|
|
|
535
727
|
return p.approvableFrom.includes(status);
|
|
536
728
|
}
|
|
537
729
|
|
|
538
|
-
// src/member.ts
|
|
539
|
-
var import_crm = require("@odla-ai/crm");
|
|
540
|
-
|
|
541
|
-
// src/application-id.ts
|
|
542
|
-
var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
|
|
543
|
-
async function applicationIdForSubmission(submissionId) {
|
|
544
|
-
const input = new TextEncoder().encode(`${APPLICATION_ID_DOMAIN}${submissionId}`);
|
|
545
|
-
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", input)).slice(0, 16);
|
|
546
|
-
bytes[6] = bytes[6] & 15 | 128;
|
|
547
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
548
|
-
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
549
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// src/member.ts
|
|
553
|
-
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
554
|
-
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
555
|
-
function resolveApplication(a) {
|
|
556
|
-
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
557
|
-
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
558
|
-
for (const [name, arr] of [["required", required], ["optional", optional]]) {
|
|
559
|
-
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
560
|
-
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
if (a?.profileFields !== void 0 && (!Array.isArray(a.profileFields) || !a.profileFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
564
|
-
throw new Error("defineChapter.application.profileFields: must be an array of field-name strings");
|
|
565
|
-
}
|
|
566
|
-
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
567
|
-
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
568
|
-
}
|
|
569
|
-
const conditions = a?.conditions ?? {};
|
|
570
|
-
for (const [field, declared] of Object.entries(conditions)) {
|
|
571
|
-
for (const key of ["visibleWhen", "requiredWhen"]) {
|
|
572
|
-
const expression = declared?.[key];
|
|
573
|
-
if (expression === void 0) continue;
|
|
574
|
-
if (typeof expression !== "string" || !expression.trim()) {
|
|
575
|
-
throw new Error(`defineChapter.application.conditions.${field}.${key}: must be a non-empty CEL condition`);
|
|
576
|
-
}
|
|
577
|
-
(0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key}`);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return {
|
|
581
|
-
required,
|
|
582
|
-
optional,
|
|
583
|
-
conditions,
|
|
584
|
-
maxLen: a?.maxLen ?? {},
|
|
585
|
-
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
586
|
-
bodyCap: a?.bodyCap ?? 32768,
|
|
587
|
-
requireDisclaimerAck: a?.requireDisclaimerAck ?? true,
|
|
588
|
-
profileFields: a?.profileFields ?? [],
|
|
589
|
-
crmFields: a?.crmFields ?? [],
|
|
590
|
-
maxArrayLen: a?.maxArrayLen ?? 100,
|
|
591
|
-
validateEmail: a?.validateEmail ?? true
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
595
|
-
function isValidEmail(value) {
|
|
596
|
-
return typeof value === "string" && EMAIL_RE.test(value);
|
|
597
|
-
}
|
|
598
|
-
function clampArray(value, max) {
|
|
599
|
-
if (!Array.isArray(value)) return value;
|
|
600
|
-
return value.filter((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean").slice(0, max);
|
|
601
|
-
}
|
|
602
|
-
function hasDisclaimerAck(fields) {
|
|
603
|
-
return fields.disclaimerAck === true || fields.disclaimerAck === "true";
|
|
604
|
-
}
|
|
605
|
-
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
606
|
-
function applicantProfile(chapter, fields) {
|
|
607
|
-
const app = chapter.application;
|
|
608
|
-
const allowed = (f) => app.profileFields.includes(f);
|
|
609
|
-
const profile = {};
|
|
610
|
-
for (const f of [...app.required, ...app.optional]) {
|
|
611
|
-
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
612
|
-
const v = fields[f];
|
|
613
|
-
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
614
|
-
}
|
|
615
|
-
if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
616
|
-
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
617
|
-
}
|
|
618
|
-
async function submitApplication(db, chapter, fields, opts) {
|
|
619
|
-
const app = chapter.application;
|
|
620
|
-
const fieldStates = (0, import_crm.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
|
|
621
|
-
for (const [f, state] of Object.entries(fieldStates)) {
|
|
622
|
-
if (!state.required) continue;
|
|
623
|
-
const v = fields[f];
|
|
624
|
-
if (typeof v !== "string" || v.trim() === "") return { ok: false, error: `${f} is required` };
|
|
625
|
-
}
|
|
626
|
-
for (const f of [...app.required, ...app.optional]) {
|
|
627
|
-
const v = fields[f];
|
|
628
|
-
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
629
|
-
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
630
|
-
}
|
|
631
|
-
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
632
|
-
return { ok: false, error: "email must be a valid email address" };
|
|
633
|
-
}
|
|
634
|
-
const acked = hasDisclaimerAck(fields);
|
|
635
|
-
if (app.requireDisclaimerAck && !acked) {
|
|
636
|
-
return { ok: false, error: "disclaimerAck is required" };
|
|
637
|
-
}
|
|
638
|
-
const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
639
|
-
const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
640
|
-
for (const f of [...app.required, ...app.optional]) {
|
|
641
|
-
if (fieldStates[f]?.visible === false) continue;
|
|
642
|
-
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
643
|
-
}
|
|
644
|
-
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
645
|
-
if (opts.groupId) row.groupId = opts.groupId;
|
|
646
|
-
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
647
|
-
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
648
|
-
return { ok: false, error: "tierId is not an offered tier" };
|
|
649
|
-
}
|
|
650
|
-
row.tierId = fields.tierId;
|
|
651
|
-
}
|
|
652
|
-
if (acked) row.disclaimerAckAt = opts.now;
|
|
653
|
-
const { duplicate } = await db.transact(
|
|
654
|
-
[{ t: "update", ns: "applications", id: id2, attrs: row }],
|
|
655
|
-
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
656
|
-
);
|
|
657
|
-
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
658
|
-
}
|
|
659
|
-
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
660
|
-
return {
|
|
661
|
-
id: group.id,
|
|
662
|
-
name: group.name,
|
|
663
|
-
standardPriceCents: group.standardPriceCents ?? 0,
|
|
664
|
-
foundingDiscountCents: group.foundingDiscountCents ?? 0,
|
|
665
|
-
tiers: [...tiers2],
|
|
666
|
-
// The browser evaluates the same conditions the server enforces.
|
|
667
|
-
conditions,
|
|
668
|
-
disclaimerText: group.disclaimerText ?? "",
|
|
669
|
-
refundPolicyText: group.refundPolicyText ?? "",
|
|
670
|
-
trustCopy: group.trustCopy ?? "",
|
|
671
|
-
commitmentText: group.commitmentText ?? "",
|
|
672
|
-
normsText: group.normsText ?? "",
|
|
673
|
-
paymentsReady: paymentsReady2
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
730
|
// src/copy-defaults-admin.ts
|
|
678
731
|
var DEFAULT_ADMIN_COPY = {
|
|
679
732
|
auth: {
|
|
@@ -1222,7 +1275,7 @@ function defineChapter(config) {
|
|
|
1222
1275
|
const copy = resolveChapterCopy(config.copy);
|
|
1223
1276
|
const network = resolveNetwork(config, crm);
|
|
1224
1277
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1225
|
-
const { schema, rules } = chapterDb(mode, auth, network.targets.length > 0);
|
|
1278
|
+
const { schema, rules } = chapterDb(mode, auth, network.targets.length > 0, application);
|
|
1226
1279
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1227
1280
|
const account = config.account ?? "none";
|
|
1228
1281
|
if (account !== "invite" && account !== "create" && account !== "none") {
|
|
@@ -2595,4 +2648,172 @@ async function loadTiers(db, group) {
|
|
|
2595
2648
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
2596
2649
|
return offerableTiers(await loadTiers(db, group), group, hasSecretKey);
|
|
2597
2650
|
}
|
|
2651
|
+
|
|
2652
|
+
// src/membership-page.ts
|
|
2653
|
+
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
2654
|
+
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2655
|
+
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
2656
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
2657
|
+
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
2658
|
+
"tierId",
|
|
2659
|
+
"variant",
|
|
2660
|
+
"badge",
|
|
2661
|
+
"summaryMarkdown",
|
|
2662
|
+
"priceEyebrow",
|
|
2663
|
+
"compareAtPriceCents",
|
|
2664
|
+
"billingLabel",
|
|
2665
|
+
"priceNoteMarkdown",
|
|
2666
|
+
"capacity",
|
|
2667
|
+
"benefits",
|
|
2668
|
+
"callout",
|
|
2669
|
+
"ctaLabel"
|
|
2670
|
+
]);
|
|
2671
|
+
var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
2672
|
+
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
2673
|
+
var MAX_TIERS = 20;
|
|
2674
|
+
var MAX_BENEFITS = 24;
|
|
2675
|
+
var MAX_MARKDOWN = 65536;
|
|
2676
|
+
var MAX_LABEL = 160;
|
|
2677
|
+
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2678
|
+
function rejectUnknownKeys(value, allowed, path, errors) {
|
|
2679
|
+
for (const key of Object.keys(value)) {
|
|
2680
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
function boundedString(value, path, errors, max, required = false) {
|
|
2684
|
+
if (value === void 0) {
|
|
2685
|
+
if (required) errors.push(`${path} is required`);
|
|
2686
|
+
return void 0;
|
|
2687
|
+
}
|
|
2688
|
+
if (typeof value !== "string") {
|
|
2689
|
+
errors.push(`${path} must be a string`);
|
|
2690
|
+
return void 0;
|
|
2691
|
+
}
|
|
2692
|
+
const text = value.trim();
|
|
2693
|
+
if (required && !text) errors.push(`${path} is required`);
|
|
2694
|
+
if (text.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2695
|
+
return text;
|
|
2696
|
+
}
|
|
2697
|
+
function validateMembershipPageDocument(input, options = {}) {
|
|
2698
|
+
const errors = [];
|
|
2699
|
+
const root = objectRecord(input);
|
|
2700
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2701
|
+
rejectUnknownKeys(root, ROOT_KEYS, "document", errors);
|
|
2702
|
+
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
2703
|
+
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
2704
|
+
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
2705
|
+
}
|
|
2706
|
+
if (!Array.isArray(root.tiers)) errors.push("document.tiers must be an array");
|
|
2707
|
+
const rawTiers = Array.isArray(root.tiers) ? root.tiers : [];
|
|
2708
|
+
if (Array.isArray(root.tiers) && rawTiers.length === 0) errors.push("document.tiers must include at least one tier");
|
|
2709
|
+
if (rawTiers.length > MAX_TIERS) errors.push(`document.tiers supports at most ${MAX_TIERS} entries`);
|
|
2710
|
+
const authority = new Map((options.tiers ?? []).map((tier) => [tier.id, tier]));
|
|
2711
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2712
|
+
const tiers2 = [];
|
|
2713
|
+
for (const [index, raw] of rawTiers.slice(0, MAX_TIERS).entries()) {
|
|
2714
|
+
const path = `document.tiers[${index}]`;
|
|
2715
|
+
const item = objectRecord(raw);
|
|
2716
|
+
if (!item) {
|
|
2717
|
+
errors.push(`${path} must be an object`);
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
rejectUnknownKeys(item, TIER_KEYS, path, errors);
|
|
2721
|
+
const tierId = boundedString(item.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2722
|
+
if (tierId && !TIER_ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2723
|
+
if (seen.has(tierId)) errors.push(`${path}.tierId duplicates "${tierId}"`);
|
|
2724
|
+
seen.add(tierId);
|
|
2725
|
+
if (authority.size && !authority.has(tierId)) errors.push(`${path}.tierId "${tierId}" is not an offered tier`);
|
|
2726
|
+
const variant = item.variant;
|
|
2727
|
+
if (variant !== void 0 && (typeof variant !== "string" || !VARIANTS.has(variant))) {
|
|
2728
|
+
errors.push(`${path}.variant must be standard, featured, or supporting`);
|
|
2729
|
+
}
|
|
2730
|
+
const compareAt = item.compareAtPriceCents;
|
|
2731
|
+
if (compareAt !== void 0 && (!Number.isInteger(compareAt) || Number(compareAt) < 0 || Number(compareAt) > 1e8)) {
|
|
2732
|
+
errors.push(`${path}.compareAtPriceCents must be an integer from 0 to 100,000,000`);
|
|
2733
|
+
}
|
|
2734
|
+
const actual = authority.get(tierId)?.priceCents;
|
|
2735
|
+
if (Number.isInteger(compareAt) && actual !== void 0 && Number(compareAt) <= actual) {
|
|
2736
|
+
errors.push(`${path}.compareAtPriceCents must be greater than the authoritative tier price`);
|
|
2737
|
+
}
|
|
2738
|
+
let benefits;
|
|
2739
|
+
if (item.benefits !== void 0) {
|
|
2740
|
+
if (!Array.isArray(item.benefits)) errors.push(`${path}.benefits must be an array`);
|
|
2741
|
+
else {
|
|
2742
|
+
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2743
|
+
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2744
|
+
const text = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
2745
|
+
return text ? [text] : [];
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
let callout;
|
|
2750
|
+
if (item.callout !== void 0) {
|
|
2751
|
+
const rawCallout = objectRecord(item.callout);
|
|
2752
|
+
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
2753
|
+
else {
|
|
2754
|
+
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
2755
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN, true);
|
|
2756
|
+
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
2757
|
+
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
let capacity;
|
|
2761
|
+
if (item.capacity !== void 0) {
|
|
2762
|
+
const rawCapacity = objectRecord(item.capacity);
|
|
2763
|
+
if (!rawCapacity) errors.push(`${path}.capacity must be an object`);
|
|
2764
|
+
else {
|
|
2765
|
+
rejectUnknownKeys(rawCapacity, CAPACITY_KEYS, `${path}.capacity`, errors);
|
|
2766
|
+
const total = rawCapacity.total;
|
|
2767
|
+
if (!Number.isInteger(total) || Number(total) < 1 || Number(total) > 1e6) {
|
|
2768
|
+
errors.push(`${path}.capacity.total must be an integer from 1 to 1,000,000`);
|
|
2769
|
+
}
|
|
2770
|
+
const label = boundedString(rawCapacity.label, `${path}.capacity.label`, errors, MAX_LABEL);
|
|
2771
|
+
if (Number.isInteger(total) && Number(total) >= 1 && Number(total) <= 1e6) {
|
|
2772
|
+
capacity = { total: Number(total), ...label ? { label } : {} };
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
2777
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN);
|
|
2778
|
+
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
2779
|
+
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
2780
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN);
|
|
2781
|
+
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
2782
|
+
tiers2.push({
|
|
2783
|
+
tierId,
|
|
2784
|
+
...typeof variant === "string" && VARIANTS.has(variant) ? { variant } : {},
|
|
2785
|
+
...badge ? { badge } : {},
|
|
2786
|
+
...summaryMarkdown ? { summaryMarkdown } : {},
|
|
2787
|
+
...priceEyebrow ? { priceEyebrow } : {},
|
|
2788
|
+
...Number.isInteger(compareAt) ? { compareAtPriceCents: Number(compareAt) } : {},
|
|
2789
|
+
...billingLabel ? { billingLabel } : {},
|
|
2790
|
+
...priceNoteMarkdown ? { priceNoteMarkdown } : {},
|
|
2791
|
+
...capacity ? { capacity } : {},
|
|
2792
|
+
...benefits ? { benefits } : {},
|
|
2793
|
+
...callout ? { callout } : {},
|
|
2794
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
if (authority.size) {
|
|
2798
|
+
for (const tierId of authority.keys()) {
|
|
2799
|
+
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN);
|
|
2803
|
+
if (errors.length) return { ok: false, errors };
|
|
2804
|
+
return {
|
|
2805
|
+
ok: true,
|
|
2806
|
+
value: {
|
|
2807
|
+
schemaVersion: 1,
|
|
2808
|
+
renderer: MEMBERSHIP_PAGE_RENDERER,
|
|
2809
|
+
tiers: tiers2,
|
|
2810
|
+
...footnoteMarkdown ? { footnoteMarkdown } : {}
|
|
2811
|
+
}
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
function membershipPageDocument(input, options = {}) {
|
|
2815
|
+
const result = validateMembershipPageDocument(input, options);
|
|
2816
|
+
if (!result.ok) throw new TypeError(`Invalid membership page document: ${result.errors.join("; ")}`);
|
|
2817
|
+
return result.value;
|
|
2818
|
+
}
|
|
2598
2819
|
//# sourceMappingURL=index.cjs.map
|