@odla-ai/chapter 0.31.2 → 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 +203 -199
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -24
- package/dist/index.d.ts +13 -24
- package/dist/index.js +201 -197
- 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 +10 -56
- 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 +10 -56
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -841,6 +841,14 @@ worker to 6 lines and deleted ~2,500 lines. The order that worked:
|
|
|
841
841
|
|
|
842
842
|
These bite silently — a smoke test won't catch them:
|
|
843
843
|
|
|
844
|
+
- **Application fields generate the application schema.** The resolved
|
|
845
|
+
`application.required` and `application.optional` lists now control both
|
|
846
|
+
submit validation and the `applications` entity emitted by
|
|
847
|
+
`defineChapter()`. Built-in fields retain their types and indexes;
|
|
848
|
+
site-defined fields become string attrs. A field removed from the reference
|
|
849
|
+
form no longer remains accidentally required at `db.transact`, and a field
|
|
850
|
+
listed in both arrays is rejected at definition time. Keep package-owned
|
|
851
|
+
operational attrs such as `status` out of both lists.
|
|
844
852
|
- **Per-field caps.** `application.defaultMaxLen` is 2000. If your form accepts
|
|
845
853
|
longer input, pass `maxLen` explicitly or the default starts rejecting it.
|
|
846
854
|
- **`services` default** is `["db","calendar","o11y"]`; `smoke` compares config
|
|
@@ -371,9 +371,9 @@ interface ResolvedPipeline {
|
|
|
371
371
|
}
|
|
372
372
|
/** The application (join form) validation surface — which string fields are
|
|
373
373
|
* required vs accepted, their max lengths, and the request body cap. Drives
|
|
374
|
-
* submit validation
|
|
375
|
-
*
|
|
376
|
-
*
|
|
374
|
+
* submit validation, the generated `applications` schema, and the CRM slot
|
|
375
|
+
* projection; defaults to the reference form. Built-in fields retain their
|
|
376
|
+
* declared schema types/indexes, and site-defined fields become string attrs. */
|
|
377
377
|
interface ChapterApplication {
|
|
378
378
|
required?: readonly string[];
|
|
379
379
|
optional?: readonly string[];
|
package/dist/index.cjs
CHANGED
|
@@ -106,7 +106,7 @@ __export(index_exports, {
|
|
|
106
106
|
slotWindow: () => slotWindow,
|
|
107
107
|
stageIndex: () => stageIndex,
|
|
108
108
|
stripeCall: () => stripeCall,
|
|
109
|
-
stripeForm: () => stripeForm,
|
|
109
|
+
stripeForm: () => import_stripe.stripeForm,
|
|
110
110
|
subAnnualCents: () => subAnnualCents,
|
|
111
111
|
submitApplication: () => submitApplication,
|
|
112
112
|
subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
|
|
@@ -116,7 +116,7 @@ __export(index_exports, {
|
|
|
116
116
|
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
117
117
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
118
118
|
validateScheduling: () => validateScheduling,
|
|
119
|
-
verifyStripeSignature: () => verifyStripeSignature,
|
|
119
|
+
verifyStripeSignature: () => import_stripe.verifyStripeSignature,
|
|
120
120
|
webhookMutationId: () => webhookMutationId
|
|
121
121
|
});
|
|
122
122
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -124,6 +124,154 @@ module.exports = __toCommonJS(index_exports);
|
|
|
124
124
|
// src/config.ts
|
|
125
125
|
var import_crm2 = require("@odla-ai/crm");
|
|
126
126
|
|
|
127
|
+
// src/member.ts
|
|
128
|
+
var import_crm = require("@odla-ai/crm");
|
|
129
|
+
|
|
130
|
+
// src/application-id.ts
|
|
131
|
+
var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
|
|
132
|
+
async function applicationIdForSubmission(submissionId) {
|
|
133
|
+
const input = new TextEncoder().encode(`${APPLICATION_ID_DOMAIN}${submissionId}`);
|
|
134
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", input)).slice(0, 16);
|
|
135
|
+
bytes[6] = bytes[6] & 15 | 128;
|
|
136
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
137
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
138
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/member.ts
|
|
142
|
+
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
143
|
+
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
144
|
+
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
145
|
+
function resolveApplication(a) {
|
|
146
|
+
const required = a?.required ?? DEFAULT_REQUIRED;
|
|
147
|
+
const optional = a?.optional ?? DEFAULT_OPTIONAL;
|
|
148
|
+
for (const [name, arr] of [["required", required], ["optional", optional]]) {
|
|
149
|
+
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
150
|
+
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
151
|
+
}
|
|
152
|
+
const unsafe = arr.find((field) => !SCHEMA_FIELD.test(field));
|
|
153
|
+
if (unsafe) {
|
|
154
|
+
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const duplicate = [...required, ...optional].find((field, index, fields) => fields.indexOf(field) !== index);
|
|
158
|
+
if (duplicate) {
|
|
159
|
+
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
160
|
+
}
|
|
161
|
+
if (a?.profileFields !== void 0 && (!Array.isArray(a.profileFields) || !a.profileFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
162
|
+
throw new Error("defineChapter.application.profileFields: must be an array of field-name strings");
|
|
163
|
+
}
|
|
164
|
+
if (a?.crmFields !== void 0 && (!Array.isArray(a.crmFields) || !a.crmFields.every((f) => typeof f === "string" && f !== ""))) {
|
|
165
|
+
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
166
|
+
}
|
|
167
|
+
const conditions = a?.conditions ?? {};
|
|
168
|
+
for (const [field, declared] of Object.entries(conditions)) {
|
|
169
|
+
for (const key of ["visibleWhen", "requiredWhen"]) {
|
|
170
|
+
const expression = declared?.[key];
|
|
171
|
+
if (expression === void 0) continue;
|
|
172
|
+
if (typeof expression !== "string" || !expression.trim()) {
|
|
173
|
+
throw new Error(`defineChapter.application.conditions.${field}.${key}: must be a non-empty CEL condition`);
|
|
174
|
+
}
|
|
175
|
+
(0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field}.${key}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
required,
|
|
180
|
+
optional,
|
|
181
|
+
conditions,
|
|
182
|
+
maxLen: a?.maxLen ?? {},
|
|
183
|
+
defaultMaxLen: a?.defaultMaxLen ?? 2e3,
|
|
184
|
+
bodyCap: a?.bodyCap ?? 32768,
|
|
185
|
+
requireDisclaimerAck: a?.requireDisclaimerAck ?? true,
|
|
186
|
+
profileFields: a?.profileFields ?? [],
|
|
187
|
+
crmFields: a?.crmFields ?? [],
|
|
188
|
+
maxArrayLen: a?.maxArrayLen ?? 100,
|
|
189
|
+
validateEmail: a?.validateEmail ?? true
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
193
|
+
function isValidEmail(value) {
|
|
194
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
195
|
+
}
|
|
196
|
+
function clampArray(value, max) {
|
|
197
|
+
if (!Array.isArray(value)) return value;
|
|
198
|
+
return value.filter((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean").slice(0, max);
|
|
199
|
+
}
|
|
200
|
+
function hasDisclaimerAck(fields) {
|
|
201
|
+
return fields.disclaimerAck === true || fields.disclaimerAck === "true";
|
|
202
|
+
}
|
|
203
|
+
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
204
|
+
function applicantProfile(chapter, fields) {
|
|
205
|
+
const app = chapter.application;
|
|
206
|
+
const allowed = (f) => app.profileFields.includes(f);
|
|
207
|
+
const profile = {};
|
|
208
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
209
|
+
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
210
|
+
const v = fields[f];
|
|
211
|
+
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
212
|
+
}
|
|
213
|
+
if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
214
|
+
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
215
|
+
}
|
|
216
|
+
async function submitApplication(db, chapter, fields, opts) {
|
|
217
|
+
const app = chapter.application;
|
|
218
|
+
const fieldStates = (0, import_crm.resolveFieldStates)(app.conditions ?? {}, fields, app.required);
|
|
219
|
+
for (const [f, state] of Object.entries(fieldStates)) {
|
|
220
|
+
if (!state.required) continue;
|
|
221
|
+
const v = fields[f];
|
|
222
|
+
if (typeof v !== "string" || v.trim() === "") return { ok: false, error: `${f} is required` };
|
|
223
|
+
}
|
|
224
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
225
|
+
const v = fields[f];
|
|
226
|
+
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
227
|
+
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
228
|
+
}
|
|
229
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
230
|
+
return { ok: false, error: "email must be a valid email address" };
|
|
231
|
+
}
|
|
232
|
+
const acked = hasDisclaimerAck(fields);
|
|
233
|
+
if (app.requireDisclaimerAck && !acked) {
|
|
234
|
+
return { ok: false, error: "disclaimerAck is required" };
|
|
235
|
+
}
|
|
236
|
+
const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
237
|
+
const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
238
|
+
for (const f of [...app.required, ...app.optional]) {
|
|
239
|
+
if (fieldStates[f]?.visible === false) continue;
|
|
240
|
+
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
241
|
+
}
|
|
242
|
+
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
243
|
+
if (opts.groupId) row.groupId = opts.groupId;
|
|
244
|
+
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
245
|
+
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
246
|
+
return { ok: false, error: "tierId is not an offered tier" };
|
|
247
|
+
}
|
|
248
|
+
row.tierId = fields.tierId;
|
|
249
|
+
}
|
|
250
|
+
if (acked) row.disclaimerAckAt = opts.now;
|
|
251
|
+
const { duplicate } = await db.transact(
|
|
252
|
+
[{ t: "update", ns: "applications", id: id2, attrs: row }],
|
|
253
|
+
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
254
|
+
);
|
|
255
|
+
return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
256
|
+
}
|
|
257
|
+
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
258
|
+
return {
|
|
259
|
+
id: group.id,
|
|
260
|
+
name: group.name,
|
|
261
|
+
standardPriceCents: group.standardPriceCents ?? 0,
|
|
262
|
+
foundingDiscountCents: group.foundingDiscountCents ?? 0,
|
|
263
|
+
tiers: [...tiers2],
|
|
264
|
+
// The browser evaluates the same conditions the server enforces.
|
|
265
|
+
conditions,
|
|
266
|
+
disclaimerText: group.disclaimerText ?? "",
|
|
267
|
+
refundPolicyText: group.refundPolicyText ?? "",
|
|
268
|
+
trustCopy: group.trustCopy ?? "",
|
|
269
|
+
commitmentText: group.commitmentText ?? "",
|
|
270
|
+
normsText: group.normsText ?? "",
|
|
271
|
+
paymentsReady: paymentsReady2
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
127
275
|
// src/schema.ts
|
|
128
276
|
function attr(type, flags = {}) {
|
|
129
277
|
return {
|
|
@@ -198,6 +346,47 @@ var applications = {
|
|
|
198
346
|
canceled: attr("boolean", { optional: true })
|
|
199
347
|
}
|
|
200
348
|
};
|
|
349
|
+
var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
350
|
+
"firstName",
|
|
351
|
+
"lastName",
|
|
352
|
+
"email",
|
|
353
|
+
"referral",
|
|
354
|
+
"referralName",
|
|
355
|
+
"whoYouAre",
|
|
356
|
+
"focus",
|
|
357
|
+
"linkedin",
|
|
358
|
+
"message",
|
|
359
|
+
"phone",
|
|
360
|
+
"state"
|
|
361
|
+
]);
|
|
362
|
+
function applicationsFor(application) {
|
|
363
|
+
const attrs = {};
|
|
364
|
+
for (const [name, spec] of Object.entries(applications.attrs)) {
|
|
365
|
+
attrs[name] = {
|
|
366
|
+
...spec,
|
|
367
|
+
optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const apply = (field, optional) => {
|
|
371
|
+
const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
|
|
372
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
|
|
373
|
+
throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
|
|
374
|
+
}
|
|
375
|
+
if (existing && existing.type !== "string") {
|
|
376
|
+
throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
377
|
+
}
|
|
378
|
+
const spec = existing ? { ...existing, optional } : attr("string", { optional });
|
|
379
|
+
Object.defineProperty(attrs, field, {
|
|
380
|
+
value: spec,
|
|
381
|
+
enumerable: true,
|
|
382
|
+
configurable: true,
|
|
383
|
+
writable: true
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
for (const field of application.required) apply(field, false);
|
|
387
|
+
for (const field of application.optional) apply(field, true);
|
|
388
|
+
return { attrs };
|
|
389
|
+
}
|
|
201
390
|
var groups = {
|
|
202
391
|
attrs: {
|
|
203
392
|
id: id(),
|
|
@@ -269,10 +458,10 @@ var emailLog = {
|
|
|
269
458
|
sentAt: attr("number", { indexed: true })
|
|
270
459
|
}
|
|
271
460
|
};
|
|
272
|
-
function chapterDb(mode, auth, includeNetworkNotes = false) {
|
|
461
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0)) {
|
|
273
462
|
const entities = {};
|
|
274
463
|
if (mode === "chapter") {
|
|
275
|
-
entities.applications =
|
|
464
|
+
entities.applications = applicationsFor(application);
|
|
276
465
|
entities.groups = groups;
|
|
277
466
|
entities.tiers = tiers;
|
|
278
467
|
entities.meetings = meetings;
|
|
@@ -535,145 +724,6 @@ function canApprove(status, p) {
|
|
|
535
724
|
return p.approvableFrom.includes(status);
|
|
536
725
|
}
|
|
537
726
|
|
|
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
727
|
// src/copy-defaults-admin.ts
|
|
678
728
|
var DEFAULT_ADMIN_COPY = {
|
|
679
729
|
auth: {
|
|
@@ -1222,7 +1272,7 @@ function defineChapter(config) {
|
|
|
1222
1272
|
const copy = resolveChapterCopy(config.copy);
|
|
1223
1273
|
const network = resolveNetwork(config, crm);
|
|
1224
1274
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1225
|
-
const { schema, rules } = chapterDb(mode, auth, network.targets.length > 0);
|
|
1275
|
+
const { schema, rules } = chapterDb(mode, auth, network.targets.length > 0, application);
|
|
1226
1276
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1227
1277
|
const account = config.account ?? "none";
|
|
1228
1278
|
if (account !== "invite" && account !== "create" && account !== "none") {
|
|
@@ -1680,53 +1730,10 @@ function emailGroupFrom(row) {
|
|
|
1680
1730
|
}
|
|
1681
1731
|
|
|
1682
1732
|
// src/payments.ts
|
|
1683
|
-
|
|
1684
|
-
const parts = {};
|
|
1685
|
-
for (const p of header.split(",")) {
|
|
1686
|
-
const [k, v] = p.split("=", 2);
|
|
1687
|
-
if (k && v !== void 0) parts[k] = v;
|
|
1688
|
-
}
|
|
1689
|
-
return { t: parts.t, v1: parts.v1 };
|
|
1690
|
-
}
|
|
1691
|
-
function toHex(buf) {
|
|
1692
|
-
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1693
|
-
}
|
|
1694
|
-
function timingSafeEqual(a, b) {
|
|
1695
|
-
if (a.length !== b.length) return false;
|
|
1696
|
-
let diff = 0;
|
|
1697
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1698
|
-
return diff === 0;
|
|
1699
|
-
}
|
|
1700
|
-
async function verifyStripeSignature(payload, header, secret, opts = {}) {
|
|
1701
|
-
const { t, v1 } = parseSigHeader(header);
|
|
1702
|
-
if (!t || !v1) return false;
|
|
1703
|
-
const ts = Number(t);
|
|
1704
|
-
if (!Number.isFinite(ts)) return false;
|
|
1705
|
-
const nowSec = (opts.now ?? Date.now()) / 1e3;
|
|
1706
|
-
const tolerance = opts.toleranceSec ?? 300;
|
|
1707
|
-
if (Math.abs(nowSec - ts) > tolerance) return false;
|
|
1708
|
-
const enc = new TextEncoder();
|
|
1709
|
-
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1710
|
-
const mac = await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${payload}`));
|
|
1711
|
-
return timingSafeEqual(toHex(mac), v1);
|
|
1712
|
-
}
|
|
1733
|
+
var import_stripe = require("@odla-ai/stripe");
|
|
1713
1734
|
function paymentsReady(group, hasSecretKey) {
|
|
1714
1735
|
return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
|
|
1715
1736
|
}
|
|
1716
|
-
function stripeForm(params) {
|
|
1717
|
-
const out = new URLSearchParams();
|
|
1718
|
-
for (const [k, v] of Object.entries(params)) {
|
|
1719
|
-
if (v === void 0 || v === null) continue;
|
|
1720
|
-
if (typeof v === "object") {
|
|
1721
|
-
for (const [k2, v2] of Object.entries(v)) {
|
|
1722
|
-
if (v2 !== void 0 && v2 !== null) out.append(`${k}[${k2}]`, String(v2));
|
|
1723
|
-
}
|
|
1724
|
-
} else {
|
|
1725
|
-
out.append(k, String(v));
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
return out.toString();
|
|
1729
|
-
}
|
|
1730
1737
|
function subscriptionIdempotencyKey(applicationId) {
|
|
1731
1738
|
return `sub:${applicationId}`;
|
|
1732
1739
|
}
|
|
@@ -2110,18 +2117,15 @@ function subAnnualCents(sub) {
|
|
|
2110
2117
|
}
|
|
2111
2118
|
|
|
2112
2119
|
// src/payments-stripe.ts
|
|
2120
|
+
var import_stripe2 = require("@odla-ai/stripe");
|
|
2113
2121
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
}
|
|
2122
|
-
const res = await fetch(`https://api.stripe.com${path}${qs}`, init);
|
|
2123
|
-
const body = await res.json().catch(() => ({}));
|
|
2124
|
-
return { ok: res.ok, status: res.status, body };
|
|
2122
|
+
return (0, import_stripe2.stripeRequest)(
|
|
2123
|
+
{ secretKey: sk },
|
|
2124
|
+
method,
|
|
2125
|
+
path,
|
|
2126
|
+
params,
|
|
2127
|
+
idempotencyKey ? { idempotencyKey } : {}
|
|
2128
|
+
);
|
|
2125
2129
|
}
|
|
2126
2130
|
|
|
2127
2131
|
// src/clerk.ts
|