@odla-ai/chapter 0.35.0 → 0.36.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/dist/{chunk-UJIVBGL5.js → chunk-I45VYBUV.js} +441 -239
- package/dist/chunk-I45VYBUV.js.map +1 -0
- package/dist/{chunk-JAWTIROV.js → chunk-IAWBE5PX.js} +55 -1
- package/dist/chunk-IAWBE5PX.js.map +1 -0
- package/dist/{chunk-TXE5ZDI4.js → chunk-LWM32REV.js} +166 -59
- package/dist/chunk-LWM32REV.js.map +1 -0
- package/dist/{copy-context-c1tqPXOR.d.ts → copy-context-0k0-yWHL.d.ts} +6 -2
- package/dist/index.cjs +843 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +200 -3
- package/dist/index.d.ts +200 -3
- package/dist/index.js +843 -293
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +10 -3
- package/dist/ui/admin/index.js +6 -2
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +11 -3
- package/dist/ui/member/index.d.ts +86 -7
- package/dist/ui/member/index.js +6 -2
- package/dist/worker/index.cjs +1806 -710
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +102 -3
- package/dist/worker/index.d.ts +102 -3
- package/dist/worker/index.js +1824 -728
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-JAWTIROV.js.map +0 -1
- package/dist/chunk-TXE5ZDI4.js.map +0 -1
- package/dist/chunk-UJIVBGL5.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -20,9 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ADMISSION_GRANTS_PATH: () => ADMISSION_GRANTS_PATH,
|
|
24
|
+
ADMISSION_GRANTS_SCHEMA_VERSION: () => ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
23
25
|
CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
|
|
24
26
|
DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
|
|
25
27
|
DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
|
|
28
|
+
GIFT_OFFER_CATALOG_VERSION: () => GIFT_OFFER_CATALOG_VERSION,
|
|
29
|
+
MAX_GIFT_OFFERS: () => MAX_GIFT_OFFERS,
|
|
26
30
|
MEMBERSHIP_PAGE_RENDERER: () => MEMBERSHIP_PAGE_RENDERER,
|
|
27
31
|
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
28
32
|
SIGNUP_CONTROL_PATH: () => SIGNUP_CONTROL_PATH,
|
|
@@ -55,6 +59,7 @@ __export(index_exports, {
|
|
|
55
59
|
clerkListUsers: () => clerkListUsers,
|
|
56
60
|
clerkSetRole: () => clerkSetRole,
|
|
57
61
|
clerkUserRequest: () => clerkUserRequest,
|
|
62
|
+
createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
|
|
58
63
|
createChapterIntegration: () => createChapterIntegration,
|
|
59
64
|
createClerkInvitation: () => createClerkInvitation,
|
|
60
65
|
createClerkUser: () => createClerkUser,
|
|
@@ -65,12 +70,15 @@ __export(index_exports, {
|
|
|
65
70
|
emailGroupFrom: () => emailGroupFrom,
|
|
66
71
|
endForSlot: () => endForSlot,
|
|
67
72
|
exportChapterPromotion: () => exportChapterPromotion,
|
|
73
|
+
findAdmissionGrant: () => findAdmissionGrant,
|
|
68
74
|
findApplicationRef: () => findApplicationRef,
|
|
75
|
+
findGiftOffer: () => findGiftOffer,
|
|
69
76
|
findTier: () => findTier,
|
|
70
77
|
firstPaymentPatch: () => firstPaymentPatch,
|
|
71
78
|
formatChapterCopy: () => formatChapterCopy,
|
|
72
79
|
formationFields: () => formationFields,
|
|
73
80
|
getVaultSecret: () => getVaultSecret,
|
|
81
|
+
giftOfferActive: () => giftOfferActive,
|
|
74
82
|
hasDisclaimerAck: () => hasDisclaimerAck,
|
|
75
83
|
hydrateSignupControlTheme: () => hydrateSignupControlTheme,
|
|
76
84
|
injectSignupControlThemeHead: () => injectSignupControlThemeHead,
|
|
@@ -83,8 +91,10 @@ __export(index_exports, {
|
|
|
83
91
|
joinConfig: () => joinConfig,
|
|
84
92
|
joinConfigTiers: () => joinConfigTiers,
|
|
85
93
|
leaderCrmConfig: () => leaderCrmConfig,
|
|
94
|
+
loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
|
|
86
95
|
loadOfferableTiers: () => loadOfferableTiers,
|
|
87
96
|
loadTiers: () => loadTiers,
|
|
97
|
+
materializeAdmissionGrants: () => materializeAdmissionGrants,
|
|
88
98
|
meetingCreateRow: () => meetingCreateRow,
|
|
89
99
|
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
90
100
|
memberApplication: () => memberApplication,
|
|
@@ -92,6 +102,7 @@ __export(index_exports, {
|
|
|
92
102
|
membershipPageDocument: () => membershipPageDocument,
|
|
93
103
|
membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
|
|
94
104
|
networkSourceTag: () => networkSourceTag,
|
|
105
|
+
normalizeAdmissionEmail: () => normalizeAdmissionEmail,
|
|
95
106
|
normalizeSharedRecord: () => normalizeSharedRecord,
|
|
96
107
|
normalizeWebhookEvent: () => normalizeWebhookEvent,
|
|
97
108
|
offerableTiers: () => offerableTiers,
|
|
@@ -100,6 +111,7 @@ __export(index_exports, {
|
|
|
100
111
|
planDelivery: () => planDelivery,
|
|
101
112
|
projectApplicant: () => projectApplicant,
|
|
102
113
|
projectSharedRecord: () => projectSharedRecord,
|
|
114
|
+
publicGiftOffers: () => publicGiftOffers,
|
|
103
115
|
reconcileMeetings: () => reconcileMeetings,
|
|
104
116
|
refundedPatch: () => refundedPatch,
|
|
105
117
|
render: () => render,
|
|
@@ -110,6 +122,7 @@ __export(index_exports, {
|
|
|
110
122
|
resolveApplication: () => resolveApplication,
|
|
111
123
|
resolveAuth: () => resolveAuth,
|
|
112
124
|
resolveChapterCopy: () => resolveChapterCopy,
|
|
125
|
+
resolveJoinContinuation: () => resolveJoinContinuation,
|
|
113
126
|
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
114
127
|
resolvePipeline: () => resolvePipeline,
|
|
115
128
|
resolveScheduling: () => resolveScheduling,
|
|
@@ -132,6 +145,8 @@ __export(index_exports, {
|
|
|
132
145
|
tierPayable: () => tierPayable,
|
|
133
146
|
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
134
147
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
148
|
+
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
149
|
+
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
135
150
|
validateMembershipPageDocument: () => validateMembershipPageDocument,
|
|
136
151
|
validateScheduling: () => validateScheduling,
|
|
137
152
|
validateSignupControlDocument: () => validateSignupControlDocument,
|
|
@@ -157,6 +172,213 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
157
172
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
158
173
|
}
|
|
159
174
|
|
|
175
|
+
// src/admission-grants-contract.ts
|
|
176
|
+
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
177
|
+
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
178
|
+
|
|
179
|
+
// src/signup-control-canonical.ts
|
|
180
|
+
var encoder = new TextEncoder();
|
|
181
|
+
function canonicalValue(value) {
|
|
182
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
183
|
+
return JSON.stringify(value);
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
186
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
187
|
+
const record2 = value;
|
|
188
|
+
const entries = Object.keys(record2).sort().flatMap((key) => {
|
|
189
|
+
const item = record2[key];
|
|
190
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
191
|
+
});
|
|
192
|
+
return `{${entries.join(",")}}`;
|
|
193
|
+
}
|
|
194
|
+
function canonicalSignupControl(value) {
|
|
195
|
+
return canonicalValue(value);
|
|
196
|
+
}
|
|
197
|
+
async function signupControlDigest(document) {
|
|
198
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
199
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
200
|
+
}
|
|
201
|
+
async function createSignupControlEnvelope(document) {
|
|
202
|
+
return { document, digest: await signupControlDigest(document) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/admission-grants.ts
|
|
206
|
+
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
207
|
+
var MAX_GRANTS = 2e3;
|
|
208
|
+
var MAX_NOTE = 2e3;
|
|
209
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
210
|
+
var isAdmissionEmail = (value) => EMAIL_RE.test(value);
|
|
211
|
+
function normalizeAdmissionEmail(email) {
|
|
212
|
+
return email.trim().toLowerCase();
|
|
213
|
+
}
|
|
214
|
+
function validateAdmissionGrantDocument(input) {
|
|
215
|
+
const errors = [];
|
|
216
|
+
const root = input && typeof input === "object" ? input : {};
|
|
217
|
+
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
218
|
+
const revision = Number(root.revision);
|
|
219
|
+
const generatedAt = Number(root.generatedAt);
|
|
220
|
+
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
221
|
+
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
222
|
+
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
223
|
+
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
224
|
+
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
225
|
+
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
226
|
+
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
227
|
+
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
228
|
+
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
229
|
+
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
230
|
+
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
231
|
+
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
232
|
+
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
233
|
+
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
234
|
+
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
235
|
+
const grants = [];
|
|
236
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
237
|
+
const seenEmails = /* @__PURE__ */ new Set();
|
|
238
|
+
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
239
|
+
const grant = raw && typeof raw === "object" ? raw : {};
|
|
240
|
+
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
241
|
+
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
242
|
+
const normalized = normalizeAdmissionEmail(email);
|
|
243
|
+
const decision = grant.decision;
|
|
244
|
+
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
245
|
+
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
246
|
+
if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
247
|
+
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
248
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
249
|
+
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
250
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
251
|
+
errors.push(`document.grants[${index}].decision is invalid`);
|
|
252
|
+
}
|
|
253
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
254
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
255
|
+
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
256
|
+
}
|
|
257
|
+
seenIds.add(id4);
|
|
258
|
+
seenEmails.add(normalized);
|
|
259
|
+
grants.push({
|
|
260
|
+
id: id4,
|
|
261
|
+
email,
|
|
262
|
+
decision,
|
|
263
|
+
...note ? { note } : {},
|
|
264
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (errors.length) return { ok: false, errors };
|
|
268
|
+
return {
|
|
269
|
+
ok: true,
|
|
270
|
+
value: {
|
|
271
|
+
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
272
|
+
revision,
|
|
273
|
+
generatedAt,
|
|
274
|
+
source: { appId: sourceId, actor: sourceActor },
|
|
275
|
+
target: { chapterId, environment },
|
|
276
|
+
grants
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async function createAdmissionGrantEnvelope(document) {
|
|
281
|
+
const checked = validateAdmissionGrantDocument(document);
|
|
282
|
+
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
283
|
+
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
284
|
+
}
|
|
285
|
+
var rows = (value) => Array.isArray(value) ? value : [];
|
|
286
|
+
function asGrant(row) {
|
|
287
|
+
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
288
|
+
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
289
|
+
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
290
|
+
return row;
|
|
291
|
+
}
|
|
292
|
+
async function findAdmissionGrant(db, targetId, email, now) {
|
|
293
|
+
const emailNormalized = normalizeAdmissionEmail(email);
|
|
294
|
+
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
295
|
+
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
296
|
+
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
297
|
+
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
298
|
+
)).sort((a, b) => {
|
|
299
|
+
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
300
|
+
return scope || b.createdAt - a.createdAt;
|
|
301
|
+
});
|
|
302
|
+
return active[0] ?? null;
|
|
303
|
+
}
|
|
304
|
+
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
305
|
+
const where = { sourceId, targetId, environment };
|
|
306
|
+
if (db.aggregate) {
|
|
307
|
+
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
308
|
+
}
|
|
309
|
+
const found = await db.query({ admissionGrantRevisions: {} });
|
|
310
|
+
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
311
|
+
}
|
|
312
|
+
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
313
|
+
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
314
|
+
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
315
|
+
where: { sourceId, targetId, environment },
|
|
316
|
+
order: { revision: "desc" },
|
|
317
|
+
limit: 1
|
|
318
|
+
} } });
|
|
319
|
+
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
320
|
+
return row ? row : null;
|
|
321
|
+
}
|
|
322
|
+
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
323
|
+
const { document, digest } = envelope;
|
|
324
|
+
const current = rows((await db.query({ admissionGrants: { $: {
|
|
325
|
+
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
326
|
+
limit: MAX_GRANTS
|
|
327
|
+
} } })).admissionGrants);
|
|
328
|
+
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
329
|
+
const ops = [];
|
|
330
|
+
for (const row of current) {
|
|
331
|
+
const id4 = String(row.id ?? "");
|
|
332
|
+
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
333
|
+
active: false,
|
|
334
|
+
revokedAt: receivedAt,
|
|
335
|
+
revokedBy: document.source.actor,
|
|
336
|
+
updatedAt: receivedAt,
|
|
337
|
+
updatedBy: document.source.actor
|
|
338
|
+
} });
|
|
339
|
+
}
|
|
340
|
+
for (const grant of document.grants) {
|
|
341
|
+
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
342
|
+
const existing = current.find((row) => row.id === id4);
|
|
343
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
344
|
+
id: id4,
|
|
345
|
+
sourceGrantId: grant.id,
|
|
346
|
+
sourceScope: "parent",
|
|
347
|
+
sourceId: document.source.appId,
|
|
348
|
+
targetId: document.target.chapterId,
|
|
349
|
+
email: grant.email,
|
|
350
|
+
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
351
|
+
decision: grant.decision,
|
|
352
|
+
...grant.note ? { note: grant.note } : {},
|
|
353
|
+
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
354
|
+
active: true,
|
|
355
|
+
sourceRevision: document.revision,
|
|
356
|
+
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
357
|
+
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
358
|
+
updatedAt: receivedAt,
|
|
359
|
+
updatedBy: document.source.actor
|
|
360
|
+
} });
|
|
361
|
+
const retract = [
|
|
362
|
+
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
363
|
+
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
364
|
+
];
|
|
365
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
366
|
+
}
|
|
367
|
+
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
368
|
+
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
369
|
+
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
370
|
+
id: revisionId,
|
|
371
|
+
sourceId: document.source.appId,
|
|
372
|
+
targetId: document.target.chapterId,
|
|
373
|
+
environment: document.target.environment,
|
|
374
|
+
revision: document.revision,
|
|
375
|
+
digest,
|
|
376
|
+
active: true,
|
|
377
|
+
receivedAt
|
|
378
|
+
} });
|
|
379
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
380
|
+
}
|
|
381
|
+
|
|
160
382
|
// src/member.ts
|
|
161
383
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
162
384
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
@@ -208,9 +430,9 @@ function resolveApplication(a) {
|
|
|
208
430
|
validateEmail: a?.validateEmail ?? true
|
|
209
431
|
};
|
|
210
432
|
}
|
|
211
|
-
var
|
|
433
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
212
434
|
function isValidEmail(value) {
|
|
213
|
-
return typeof value === "string" &&
|
|
435
|
+
return typeof value === "string" && EMAIL_RE2.test(value);
|
|
214
436
|
}
|
|
215
437
|
function clampArray(value, max) {
|
|
216
438
|
if (!Array.isArray(value)) return value;
|
|
@@ -252,14 +474,23 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
252
474
|
if (app.requireDisclaimerAck && !acked) {
|
|
253
475
|
return { ok: false, error: "disclaimerAck is required" };
|
|
254
476
|
}
|
|
255
|
-
const
|
|
256
|
-
const row = { id:
|
|
477
|
+
const id4 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
478
|
+
const row = { id: id4, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
257
479
|
for (const f of [...app.required, ...app.optional]) {
|
|
258
480
|
if (fieldStates[f]?.visible === false) continue;
|
|
259
481
|
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
260
482
|
}
|
|
261
483
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
262
484
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
485
|
+
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
486
|
+
row.emailNormalized = normalizeAdmissionEmail(fields.email);
|
|
487
|
+
}
|
|
488
|
+
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
489
|
+
row.admissionGrantId = opts.admissionGrant.id;
|
|
490
|
+
row.admissionSource = opts.admissionGrant.sourceScope;
|
|
491
|
+
row.interviewWaivedAt = opts.now;
|
|
492
|
+
}
|
|
493
|
+
if (opts.giftOfferId) row.giftOfferId = opts.giftOfferId;
|
|
263
494
|
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
264
495
|
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
265
496
|
return { ok: false, error: "tierId is not an offered tier" };
|
|
@@ -267,11 +498,20 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
267
498
|
row.tierId = fields.tierId;
|
|
268
499
|
}
|
|
269
500
|
if (acked) row.disclaimerAckAt = opts.now;
|
|
501
|
+
const ops = [
|
|
502
|
+
{ t: "update", ns: "applications", id: id4, attrs: row }
|
|
503
|
+
];
|
|
504
|
+
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
505
|
+
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
506
|
+
consumedAt: opts.now,
|
|
507
|
+
consumedApplicationId: id4
|
|
508
|
+
} });
|
|
509
|
+
}
|
|
270
510
|
const { duplicate } = await db.transact(
|
|
271
|
-
|
|
511
|
+
ops,
|
|
272
512
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
273
513
|
);
|
|
274
|
-
return { ok: true, id:
|
|
514
|
+
return { ok: true, id: id4, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
275
515
|
}
|
|
276
516
|
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
277
517
|
return {
|
|
@@ -291,93 +531,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
|
291
531
|
};
|
|
292
532
|
}
|
|
293
533
|
|
|
294
|
-
// src/schema.ts
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
var id = () => attr("string", { unique: true, indexed: true });
|
|
304
|
-
var admins = {
|
|
305
|
-
attrs: {
|
|
306
|
-
id: id(),
|
|
307
|
-
email: attr("string", { unique: true, indexed: true }),
|
|
308
|
-
name: attr("string", { optional: true }),
|
|
309
|
-
note: attr("string", { optional: true })
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
var superAdmins = {
|
|
313
|
-
attrs: {
|
|
314
|
-
id: id(),
|
|
315
|
-
email: attr("string", { unique: true, indexed: true }),
|
|
316
|
-
note: attr("string", { optional: true }),
|
|
317
|
-
createdAt: attr("number", { indexed: true })
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
var networkNotes = {
|
|
321
|
-
attrs: {
|
|
322
|
-
id: id(),
|
|
323
|
-
recordKey: attr("string", { indexed: true }),
|
|
324
|
-
targetId: attr("string", { indexed: true }),
|
|
325
|
-
recordType: attr("string", { indexed: true }),
|
|
326
|
-
recordId: attr("string", { indexed: true }),
|
|
327
|
-
body: attr("string"),
|
|
328
|
-
authorId: attr("string", { indexed: true }),
|
|
329
|
-
authorEmail: attr("string", { optional: true }),
|
|
330
|
-
createdAt: attr("number", { indexed: true })
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
var signupControls = {
|
|
334
|
-
attrs: {
|
|
335
|
-
id: id(),
|
|
336
|
-
sourceId: attr("string", { indexed: true }),
|
|
337
|
-
targetId: attr("string", { indexed: true }),
|
|
338
|
-
environment: attr("string", { indexed: true }),
|
|
339
|
-
revision: attr("number", { indexed: true }),
|
|
340
|
-
digest: attr("string", { indexed: true }),
|
|
341
|
-
document: attr("json"),
|
|
342
|
-
active: attr("boolean", { indexed: true }),
|
|
343
|
-
receivedAt: attr("number", { indexed: true })
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
var applications = {
|
|
347
|
-
attrs: {
|
|
348
|
-
id: id(),
|
|
349
|
-
firstName: attr("string"),
|
|
350
|
-
lastName: attr("string"),
|
|
351
|
-
email: attr("string", { indexed: true }),
|
|
352
|
-
referral: attr("string"),
|
|
353
|
-
referralName: attr("string", { optional: true }),
|
|
354
|
-
whoYouAre: attr("string"),
|
|
355
|
-
focus: attr("json"),
|
|
356
|
-
linkedin: attr("string", { optional: true }),
|
|
357
|
-
message: attr("string"),
|
|
358
|
-
status: attr("string", { indexed: true }),
|
|
359
|
-
createdAt: attr("number", { indexed: true }),
|
|
360
|
-
meetingAt: attr("number", { indexed: true, optional: true }),
|
|
361
|
-
meetingLink: attr("string", { optional: true }),
|
|
362
|
-
clerkUserId: attr("string", { indexed: true, optional: true }),
|
|
363
|
-
// One-time/effect-repair marker. Absent means /api/me should copy the
|
|
364
|
-
// allowlisted application snapshot into Clerk private_metadata and remove
|
|
365
|
-
// the former public_metadata copy.
|
|
366
|
-
clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
|
|
367
|
-
phone: attr("string", { optional: true }),
|
|
368
|
-
state: attr("string", { optional: true }),
|
|
369
|
-
groupId: attr("string", { indexed: true, optional: true }),
|
|
370
|
-
// The tier this application joined on. Absent on rows that predate tiers.
|
|
371
|
-
tierId: attr("string", { indexed: true, optional: true }),
|
|
372
|
-
stripeCustomerId: attr("string", { indexed: true, optional: true }),
|
|
373
|
-
stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
|
|
374
|
-
renewalAt: attr("number", { optional: true }),
|
|
375
|
-
disclaimerAckAt: attr("number", { optional: true }),
|
|
376
|
-
refundPolicyAckAt: attr("number", { optional: true }),
|
|
377
|
-
prepEmailSentAt: attr("number", { optional: true }),
|
|
378
|
-
canceled: attr("boolean", { optional: true })
|
|
379
|
-
}
|
|
380
|
-
};
|
|
534
|
+
// src/schema-applications.ts
|
|
535
|
+
var attr = (type, flags = {}) => ({
|
|
536
|
+
type,
|
|
537
|
+
unique: flags.unique ?? false,
|
|
538
|
+
indexed: flags.indexed ?? false,
|
|
539
|
+
optional: flags.optional ?? false
|
|
540
|
+
});
|
|
381
541
|
var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
382
542
|
"firstName",
|
|
383
543
|
"lastName",
|
|
@@ -391,25 +551,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
391
551
|
"phone",
|
|
392
552
|
"state"
|
|
393
553
|
]);
|
|
554
|
+
var applicationAttrs = {
|
|
555
|
+
id: attr("string", { unique: true, indexed: true }),
|
|
556
|
+
firstName: attr("string"),
|
|
557
|
+
lastName: attr("string"),
|
|
558
|
+
email: attr("string", { indexed: true }),
|
|
559
|
+
referral: attr("string"),
|
|
560
|
+
referralName: attr("string", { optional: true }),
|
|
561
|
+
whoYouAre: attr("string"),
|
|
562
|
+
focus: attr("json"),
|
|
563
|
+
linkedin: attr("string", { optional: true }),
|
|
564
|
+
message: attr("string"),
|
|
565
|
+
status: attr("string", { indexed: true }),
|
|
566
|
+
createdAt: attr("number", { indexed: true }),
|
|
567
|
+
meetingAt: attr("number", { indexed: true, optional: true }),
|
|
568
|
+
meetingLink: attr("string", { optional: true }),
|
|
569
|
+
clerkUserId: attr("string", { indexed: true, optional: true }),
|
|
570
|
+
clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
|
|
571
|
+
phone: attr("string", { optional: true }),
|
|
572
|
+
state: attr("string", { optional: true }),
|
|
573
|
+
groupId: attr("string", { indexed: true, optional: true }),
|
|
574
|
+
tierId: attr("string", { indexed: true, optional: true }),
|
|
575
|
+
stripeCustomerId: attr("string", { indexed: true, optional: true }),
|
|
576
|
+
stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
|
|
577
|
+
renewalAt: attr("number", { optional: true }),
|
|
578
|
+
disclaimerAckAt: attr("number", { optional: true }),
|
|
579
|
+
refundPolicyAckAt: attr("number", { optional: true }),
|
|
580
|
+
prepEmailSentAt: attr("number", { optional: true }),
|
|
581
|
+
canceled: attr("boolean", { optional: true }),
|
|
582
|
+
emailNormalized: attr("string", { indexed: true, optional: true }),
|
|
583
|
+
admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
|
|
584
|
+
admissionSource: attr("string", { indexed: true, optional: true }),
|
|
585
|
+
interviewWaivedAt: attr("number", { optional: true }),
|
|
586
|
+
giftOfferId: attr("string", { optional: true, indexed: true }),
|
|
587
|
+
giftIntentId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
588
|
+
giftClaimId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
589
|
+
giftOrderId: attr("string", { optional: true, indexed: true }),
|
|
590
|
+
giftPaidAt: attr("number", { optional: true, indexed: true }),
|
|
591
|
+
giftRevokedAt: attr("number", { optional: true, indexed: true }),
|
|
592
|
+
giftCheckoutStartedAt: attr("number", { optional: true, indexed: true })
|
|
593
|
+
};
|
|
394
594
|
function applicationsFor(application) {
|
|
395
595
|
const attrs = {};
|
|
396
|
-
for (const [name, spec] of Object.entries(
|
|
397
|
-
attrs[name] = {
|
|
398
|
-
...spec,
|
|
399
|
-
optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
|
|
400
|
-
};
|
|
596
|
+
for (const [name, spec] of Object.entries(applicationAttrs)) {
|
|
597
|
+
attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
|
|
401
598
|
}
|
|
402
599
|
const apply = (field, optional) => {
|
|
403
600
|
const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
|
|
404
|
-
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
if (existing && existing.type !== "string") {
|
|
408
|
-
throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
409
|
-
}
|
|
410
|
-
const spec = existing ? { ...existing, optional } : attr("string", { optional });
|
|
601
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
|
|
602
|
+
if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
411
603
|
Object.defineProperty(attrs, field, {
|
|
412
|
-
value:
|
|
604
|
+
value: existing ? { ...existing, optional } : attr("string", { optional }),
|
|
413
605
|
enumerable: true,
|
|
414
606
|
configurable: true,
|
|
415
607
|
writable: true
|
|
@@ -419,75 +611,270 @@ function applicationsFor(application) {
|
|
|
419
611
|
for (const field of application.optional) apply(field, true);
|
|
420
612
|
return { attrs };
|
|
421
613
|
}
|
|
422
|
-
|
|
614
|
+
|
|
615
|
+
// src/schema-admission.ts
|
|
616
|
+
var attr2 = (type, flags = {}) => ({
|
|
617
|
+
type,
|
|
618
|
+
unique: flags.unique ?? false,
|
|
619
|
+
indexed: flags.indexed ?? false,
|
|
620
|
+
optional: flags.optional ?? false
|
|
621
|
+
});
|
|
622
|
+
var id = () => attr2("string", { unique: true, indexed: true });
|
|
623
|
+
var admissionGrantsEntity = {
|
|
423
624
|
attrs: {
|
|
424
625
|
id: id(),
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
626
|
+
sourceGrantId: attr2("string", { indexed: true }),
|
|
627
|
+
sourceScope: attr2("string", { indexed: true }),
|
|
628
|
+
sourceId: attr2("string", { indexed: true }),
|
|
629
|
+
targetId: attr2("string", { indexed: true }),
|
|
630
|
+
email: attr2("string"),
|
|
631
|
+
emailNormalized: attr2("string", { indexed: true }),
|
|
632
|
+
decision: attr2("string", { indexed: true }),
|
|
633
|
+
note: attr2("string", { optional: true }),
|
|
634
|
+
active: attr2("boolean", { indexed: true }),
|
|
635
|
+
sourceRevision: attr2("number", { optional: true, indexed: true }),
|
|
636
|
+
createdAt: attr2("number", { indexed: true }),
|
|
637
|
+
createdBy: attr2("string", { optional: true, indexed: true }),
|
|
638
|
+
updatedAt: attr2("number", { optional: true, indexed: true }),
|
|
639
|
+
updatedBy: attr2("string", { optional: true, indexed: true }),
|
|
640
|
+
expiresAt: attr2("number", { optional: true, indexed: true }),
|
|
641
|
+
revokedAt: attr2("number", { optional: true, indexed: true }),
|
|
642
|
+
revokedBy: attr2("string", { optional: true, indexed: true }),
|
|
643
|
+
consumedAt: attr2("number", { optional: true, indexed: true }),
|
|
644
|
+
consumedApplicationId: attr2("string", { optional: true, indexed: true })
|
|
442
645
|
}
|
|
443
646
|
};
|
|
444
|
-
var
|
|
647
|
+
var admissionGrantRevisionsEntity = {
|
|
445
648
|
attrs: {
|
|
446
649
|
id: id(),
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
650
|
+
sourceId: attr2("string", { indexed: true }),
|
|
651
|
+
targetId: attr2("string", { indexed: true }),
|
|
652
|
+
environment: attr2("string", { indexed: true }),
|
|
653
|
+
revision: attr2("number", { indexed: true }),
|
|
654
|
+
digest: attr2("string", { indexed: true }),
|
|
655
|
+
active: attr2("boolean", { indexed: true }),
|
|
656
|
+
receivedAt: attr2("number", { indexed: true })
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// src/schema-gifts.ts
|
|
661
|
+
var attr3 = (type, flags = {}) => ({
|
|
662
|
+
type,
|
|
663
|
+
unique: flags.unique ?? false,
|
|
664
|
+
indexed: flags.indexed ?? false,
|
|
665
|
+
optional: flags.optional ?? false
|
|
666
|
+
});
|
|
667
|
+
var id2 = () => attr3("string", { unique: true, indexed: true });
|
|
668
|
+
var giftPurchaseIntentsEntity = { attrs: {
|
|
669
|
+
id: id2(),
|
|
670
|
+
chapterId: attr3("string", { indexed: true }),
|
|
671
|
+
environment: attr3("string", { indexed: true }),
|
|
672
|
+
purchaserType: attr3("string", { indexed: true }),
|
|
673
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
674
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
675
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
676
|
+
offerId: attr3("string", { indexed: true }),
|
|
677
|
+
offerSnapshot: attr3("json"),
|
|
678
|
+
status: attr3("string", { indexed: true }),
|
|
679
|
+
codeHash: attr3("string", { indexed: true }),
|
|
680
|
+
codeVersion: attr3("number"),
|
|
681
|
+
codeIssuedAt: attr3("number"),
|
|
682
|
+
providerObjectId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
683
|
+
providerCustomerId: attr3("string", { optional: true, indexed: true }),
|
|
684
|
+
checkoutStartedAt: attr3("number", { optional: true }),
|
|
685
|
+
paidAt: attr3("number", { optional: true, indexed: true }),
|
|
686
|
+
claimId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
687
|
+
createdAt: attr3("number", { indexed: true }),
|
|
688
|
+
updatedAt: attr3("number", { indexed: true })
|
|
689
|
+
} };
|
|
690
|
+
var giftOrdersEntity = { attrs: {
|
|
691
|
+
id: id2(),
|
|
692
|
+
intentId: attr3("string", { unique: true, indexed: true }),
|
|
693
|
+
chapterId: attr3("string", { indexed: true }),
|
|
694
|
+
environment: attr3("string", { indexed: true }),
|
|
695
|
+
offerId: attr3("string", { indexed: true }),
|
|
696
|
+
offerSnapshot: attr3("json"),
|
|
697
|
+
purchaserType: attr3("string", { indexed: true }),
|
|
698
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
699
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
700
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
701
|
+
providerEventId: attr3("string", { unique: true, indexed: true }),
|
|
702
|
+
providerObjectId: attr3("string", { indexed: true }),
|
|
703
|
+
amountCents: attr3("number"),
|
|
704
|
+
currency: attr3("string"),
|
|
705
|
+
billingMode: attr3("string"),
|
|
706
|
+
createdAt: attr3("number", { indexed: true })
|
|
707
|
+
} };
|
|
708
|
+
var giftClaimsEntity = { attrs: {
|
|
709
|
+
id: id2(),
|
|
710
|
+
orderId: attr3("string", { unique: true, indexed: true }),
|
|
711
|
+
intentId: attr3("string", { unique: true, indexed: true }),
|
|
712
|
+
chapterId: attr3("string", { indexed: true }),
|
|
713
|
+
offerId: attr3("string", { indexed: true }),
|
|
714
|
+
target: attr3("json"),
|
|
715
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
716
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
717
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
718
|
+
codeHash: attr3("string", { unique: true, indexed: true }),
|
|
719
|
+
codeVersion: attr3("number"),
|
|
720
|
+
rotationCount: attr3("number"),
|
|
721
|
+
codeIssuedAt: attr3("number"),
|
|
722
|
+
codeExpiresAt: attr3("number", { indexed: true }),
|
|
723
|
+
attemptCount: attr3("number"),
|
|
724
|
+
attemptWindowStartedAt: attr3("number"),
|
|
725
|
+
status: attr3("string", { indexed: true }),
|
|
726
|
+
redeemedAt: attr3("number", { optional: true, indexed: true }),
|
|
727
|
+
beneficiaryEmailNormalized: attr3("string", { optional: true, indexed: true }),
|
|
728
|
+
beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
|
|
729
|
+
applicationId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
730
|
+
entitlementId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
731
|
+
rotatedAt: attr3("number", { optional: true }),
|
|
732
|
+
revokedAt: attr3("number", { optional: true, indexed: true }),
|
|
733
|
+
revokedReason: attr3("string", { optional: true }),
|
|
734
|
+
createdAt: attr3("number", { indexed: true })
|
|
735
|
+
} };
|
|
736
|
+
var giftEntitlementsEntity = { attrs: {
|
|
737
|
+
id: id2(),
|
|
738
|
+
claimId: attr3("string", { unique: true, indexed: true }),
|
|
739
|
+
orderId: attr3("string", { indexed: true }),
|
|
740
|
+
chapterId: attr3("string", { indexed: true }),
|
|
741
|
+
entitlementKey: attr3("string", { indexed: true }),
|
|
742
|
+
beneficiaryEmailNormalized: attr3("string", { indexed: true }),
|
|
743
|
+
beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
|
|
744
|
+
active: attr3("boolean", { indexed: true }),
|
|
745
|
+
createdAt: attr3("number", { indexed: true }),
|
|
746
|
+
revokedAt: attr3("number", { optional: true })
|
|
747
|
+
} };
|
|
748
|
+
var giftRedemptionAttemptsEntity = { attrs: {
|
|
749
|
+
id: id2(),
|
|
750
|
+
chapterId: attr3("string", { indexed: true }),
|
|
751
|
+
bucket: attr3("string", { indexed: true }),
|
|
752
|
+
attemptCount: attr3("number"),
|
|
753
|
+
windowStartedAt: attr3("number", { indexed: true }),
|
|
754
|
+
updatedAt: attr3("number")
|
|
755
|
+
} };
|
|
756
|
+
|
|
757
|
+
// src/schema.ts
|
|
758
|
+
function attr4(type, flags = {}) {
|
|
759
|
+
return {
|
|
760
|
+
type,
|
|
761
|
+
unique: flags.unique ?? false,
|
|
762
|
+
indexed: flags.indexed ?? false,
|
|
763
|
+
optional: flags.optional ?? false
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
var id3 = () => attr4("string", { unique: true, indexed: true });
|
|
767
|
+
var admins = {
|
|
768
|
+
attrs: {
|
|
769
|
+
id: id3(),
|
|
770
|
+
email: attr4("string", { unique: true, indexed: true }),
|
|
771
|
+
name: attr4("string", { optional: true }),
|
|
772
|
+
note: attr4("string", { optional: true })
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
var superAdmins = {
|
|
776
|
+
attrs: {
|
|
777
|
+
id: id3(),
|
|
778
|
+
email: attr4("string", { unique: true, indexed: true }),
|
|
779
|
+
note: attr4("string", { optional: true }),
|
|
780
|
+
createdAt: attr4("number", { indexed: true })
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
var networkNotes = {
|
|
784
|
+
attrs: {
|
|
785
|
+
id: id3(),
|
|
786
|
+
recordKey: attr4("string", { indexed: true }),
|
|
787
|
+
targetId: attr4("string", { indexed: true }),
|
|
788
|
+
recordType: attr4("string", { indexed: true }),
|
|
789
|
+
recordId: attr4("string", { indexed: true }),
|
|
790
|
+
body: attr4("string"),
|
|
791
|
+
authorId: attr4("string", { indexed: true }),
|
|
792
|
+
authorEmail: attr4("string", { optional: true }),
|
|
793
|
+
createdAt: attr4("number", { indexed: true })
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
var signupControls = {
|
|
797
|
+
attrs: {
|
|
798
|
+
id: id3(),
|
|
799
|
+
sourceId: attr4("string", { indexed: true }),
|
|
800
|
+
targetId: attr4("string", { indexed: true }),
|
|
801
|
+
environment: attr4("string", { indexed: true }),
|
|
802
|
+
revision: attr4("number", { indexed: true }),
|
|
803
|
+
digest: attr4("string", { indexed: true }),
|
|
804
|
+
document: attr4("json"),
|
|
805
|
+
active: attr4("boolean", { indexed: true }),
|
|
806
|
+
receivedAt: attr4("number", { indexed: true })
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
var groups = {
|
|
810
|
+
attrs: {
|
|
811
|
+
id: id3(),
|
|
812
|
+
name: attr4("string"),
|
|
813
|
+
standardPriceCents: attr4("number"),
|
|
814
|
+
foundingDiscountCents: attr4("number"),
|
|
815
|
+
stripePriceId: attr4("string", { optional: true }),
|
|
816
|
+
stripePublishableKey: attr4("string", { optional: true }),
|
|
817
|
+
notificationEmail: attr4("string"),
|
|
818
|
+
replyTo: attr4("string"),
|
|
819
|
+
debugEmail: attr4("string", { optional: true }),
|
|
820
|
+
calendarLink: attr4("string", { optional: true }),
|
|
821
|
+
disclaimerText: attr4("string"),
|
|
822
|
+
refundPolicyText: attr4("string"),
|
|
823
|
+
trustCopy: attr4("string"),
|
|
824
|
+
commitmentText: attr4("string", { optional: true }),
|
|
825
|
+
normsText: attr4("string", { optional: true }),
|
|
826
|
+
emailTemplates: attr4("json"),
|
|
827
|
+
schedulingJson: attr4("json", { optional: true }),
|
|
828
|
+
createdAt: attr4("number", { indexed: true })
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
var tiers = {
|
|
832
|
+
attrs: {
|
|
833
|
+
id: id3(),
|
|
834
|
+
groupId: attr4("string", { indexed: true }),
|
|
835
|
+
name: attr4("string"),
|
|
836
|
+
priceCents: attr4("number"),
|
|
837
|
+
stripePriceId: attr4("string", { optional: true }),
|
|
838
|
+
blurb: attr4("string", { optional: true }),
|
|
839
|
+
sortOrder: attr4("number", { indexed: true }),
|
|
840
|
+
active: attr4("boolean", { indexed: true }),
|
|
841
|
+
createdAt: attr4("number", { indexed: true })
|
|
455
842
|
}
|
|
456
843
|
};
|
|
457
844
|
var meetings = {
|
|
458
845
|
attrs: {
|
|
459
|
-
id:
|
|
460
|
-
applicationId:
|
|
461
|
-
groupId:
|
|
462
|
-
startAt:
|
|
463
|
-
endAt:
|
|
464
|
-
timezone:
|
|
465
|
-
status:
|
|
466
|
-
googleEventId:
|
|
467
|
-
meetUrl:
|
|
468
|
-
htmlLink:
|
|
469
|
-
drift:
|
|
470
|
-
driftGoogleStartAt:
|
|
471
|
-
driftDetectedAt:
|
|
472
|
-
adoptedFromGoogleAt:
|
|
473
|
-
createdAt:
|
|
846
|
+
id: id3(),
|
|
847
|
+
applicationId: attr4("string", { indexed: true }),
|
|
848
|
+
groupId: attr4("string", { indexed: true }),
|
|
849
|
+
startAt: attr4("number", { indexed: true }),
|
|
850
|
+
endAt: attr4("number"),
|
|
851
|
+
timezone: attr4("string"),
|
|
852
|
+
status: attr4("string", { indexed: true }),
|
|
853
|
+
googleEventId: attr4("string", { indexed: true, optional: true }),
|
|
854
|
+
meetUrl: attr4("string", { optional: true }),
|
|
855
|
+
htmlLink: attr4("string", { optional: true }),
|
|
856
|
+
drift: attr4("string", { indexed: true, optional: true }),
|
|
857
|
+
driftGoogleStartAt: attr4("number", { optional: true }),
|
|
858
|
+
driftDetectedAt: attr4("number", { optional: true }),
|
|
859
|
+
adoptedFromGoogleAt: attr4("number", { optional: true }),
|
|
860
|
+
createdAt: attr4("number", { indexed: true })
|
|
474
861
|
}
|
|
475
862
|
};
|
|
476
863
|
var emailLog = {
|
|
477
864
|
attrs: {
|
|
478
|
-
id:
|
|
479
|
-
groupId:
|
|
480
|
-
applicationId:
|
|
481
|
-
to:
|
|
482
|
-
template:
|
|
483
|
-
subject:
|
|
484
|
-
body:
|
|
485
|
-
transport:
|
|
486
|
-
messageId:
|
|
487
|
-
redirected:
|
|
488
|
-
dedupeKey:
|
|
489
|
-
error:
|
|
490
|
-
sentAt:
|
|
865
|
+
id: id3(),
|
|
866
|
+
groupId: attr4("string", { indexed: true }),
|
|
867
|
+
applicationId: attr4("string", { indexed: true, optional: true }),
|
|
868
|
+
to: attr4("string", { indexed: true }),
|
|
869
|
+
template: attr4("string", { indexed: true }),
|
|
870
|
+
subject: attr4("string"),
|
|
871
|
+
body: attr4("string", { optional: true }),
|
|
872
|
+
transport: attr4("string"),
|
|
873
|
+
messageId: attr4("string", { optional: true }),
|
|
874
|
+
redirected: attr4("boolean", { optional: true }),
|
|
875
|
+
dedupeKey: attr4("string", { indexed: true, optional: true }),
|
|
876
|
+
error: attr4("string", { optional: true }),
|
|
877
|
+
sentAt: attr4("number", { indexed: true })
|
|
491
878
|
}
|
|
492
879
|
};
|
|
493
880
|
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
@@ -498,6 +885,13 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
498
885
|
entities.tiers = tiers;
|
|
499
886
|
entities.meetings = meetings;
|
|
500
887
|
entities.emailLog = emailLog;
|
|
888
|
+
entities.admissionGrants = admissionGrantsEntity;
|
|
889
|
+
entities.admissionGrantRevisions = admissionGrantRevisionsEntity;
|
|
890
|
+
entities.giftPurchaseIntents = giftPurchaseIntentsEntity;
|
|
891
|
+
entities.giftOrders = giftOrdersEntity;
|
|
892
|
+
entities.giftClaims = giftClaimsEntity;
|
|
893
|
+
entities.giftEntitlements = giftEntitlementsEntity;
|
|
894
|
+
entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
|
|
501
895
|
if (includeSignupControl) entities.signupControls = signupControls;
|
|
502
896
|
}
|
|
503
897
|
if (auth.source === "table") entities.admins = admins;
|
|
@@ -782,6 +1176,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
782
1176
|
overview: "Overview",
|
|
783
1177
|
billing: "Billing",
|
|
784
1178
|
people: "People",
|
|
1179
|
+
admission: "Admission",
|
|
785
1180
|
network: "Network",
|
|
786
1181
|
portfolio: "Portfolio",
|
|
787
1182
|
settings: "Settings",
|
|
@@ -892,6 +1287,26 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
892
1287
|
reschedule: "Reschedule",
|
|
893
1288
|
cancel: "Cancel"
|
|
894
1289
|
},
|
|
1290
|
+
admission: {
|
|
1291
|
+
title: "Interview decisions",
|
|
1292
|
+
description: "Local decisions override Parent decisions. Requiring an interview locally suppresses a Parent waiver for the same email.",
|
|
1293
|
+
email: "Email",
|
|
1294
|
+
decision: "Decision",
|
|
1295
|
+
expires: "Expires",
|
|
1296
|
+
note: "Internal note",
|
|
1297
|
+
waive: "Waive interview",
|
|
1298
|
+
require: "Require interview",
|
|
1299
|
+
add: "Add decision",
|
|
1300
|
+
ledger: "Effective grant ledger",
|
|
1301
|
+
loading: "Loading\u2026",
|
|
1302
|
+
consumed: "consumed",
|
|
1303
|
+
revoked: "revoked",
|
|
1304
|
+
expired: "expired",
|
|
1305
|
+
active: "active",
|
|
1306
|
+
local: "local",
|
|
1307
|
+
parent: "Parent",
|
|
1308
|
+
revoke: "Revoke"
|
|
1309
|
+
},
|
|
895
1310
|
network: {
|
|
896
1311
|
title: "Share to a follower",
|
|
897
1312
|
allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
|
|
@@ -990,6 +1405,19 @@ var DEFAULT_JOIN_COPY = {
|
|
|
990
1405
|
label: "You're booked",
|
|
991
1406
|
calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
|
|
992
1407
|
memberArea: "Go to your member area"
|
|
1408
|
+
},
|
|
1409
|
+
accepted: {
|
|
1410
|
+
label: "You're approved",
|
|
1411
|
+
body: "Your membership is approved. No introduction call is required."
|
|
1412
|
+
},
|
|
1413
|
+
gift: {
|
|
1414
|
+
chooseSelf: "Buy membership for myself",
|
|
1415
|
+
optionsLabel: "Or give access to someone else",
|
|
1416
|
+
label: "Your gift code",
|
|
1417
|
+
body: "Share this one-time code with the recipient. It becomes active after payment is confirmed.",
|
|
1418
|
+
save: "Save it now. For security, this code is shown only once.",
|
|
1419
|
+
rotate: "Generate a new code",
|
|
1420
|
+
rotateFailed: "A new code could not be generated."
|
|
993
1421
|
}
|
|
994
1422
|
};
|
|
995
1423
|
var DEFAULT_MEMBERS_COPY = {
|
|
@@ -1017,6 +1445,26 @@ var DEFAULT_MEMBERS_COPY = {
|
|
|
1017
1445
|
full: {
|
|
1018
1446
|
welcome: "Welcome back."
|
|
1019
1447
|
},
|
|
1448
|
+
gifts: {
|
|
1449
|
+
title: "Give access",
|
|
1450
|
+
body: "Purchase access for someone else and share the one-time claim code.",
|
|
1451
|
+
noneAvailable: "No gift options are available right now.",
|
|
1452
|
+
paid: "Payment submitted. Save the code below while it is visible.",
|
|
1453
|
+
saveCode: "This code is shown once. Save it before leaving this page.",
|
|
1454
|
+
purchases: "Your gifts",
|
|
1455
|
+
noPurchases: "No gift purchases yet.",
|
|
1456
|
+
pending: "Pending",
|
|
1457
|
+
redeemed: "Redeemed",
|
|
1458
|
+
expired: "Expired",
|
|
1459
|
+
revoked: "Revoked",
|
|
1460
|
+
rotate: "Rotate code",
|
|
1461
|
+
rotated: "A new code was generated. Save it now.",
|
|
1462
|
+
rotateFailed: "The code could not be rotated.",
|
|
1463
|
+
redeemTitle: "Redeem a gift",
|
|
1464
|
+
code: "One-time code",
|
|
1465
|
+
redeem: "Redeem",
|
|
1466
|
+
redeemFailed: "The code is invalid or unavailable."
|
|
1467
|
+
},
|
|
1020
1468
|
reschedule: {
|
|
1021
1469
|
changeTime: "Change your time",
|
|
1022
1470
|
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
@@ -1132,13 +1580,13 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1132
1580
|
}
|
|
1133
1581
|
function formationFields(crm, formation) {
|
|
1134
1582
|
const def = crm.type(formation.type);
|
|
1135
|
-
return [...formation.required, ...formation.optional].map((
|
|
1136
|
-
const field = def.fields[
|
|
1583
|
+
return [...formation.required, ...formation.optional].map((id4) => {
|
|
1584
|
+
const field = def.fields[id4];
|
|
1137
1585
|
return {
|
|
1138
|
-
id:
|
|
1139
|
-
label: field.label ??
|
|
1586
|
+
id: id4,
|
|
1587
|
+
label: field.label ?? id4,
|
|
1140
1588
|
type: field.type,
|
|
1141
|
-
required: formation.required.includes(
|
|
1589
|
+
required: formation.required.includes(id4),
|
|
1142
1590
|
...field.options ? { options: field.options } : {}
|
|
1143
1591
|
};
|
|
1144
1592
|
});
|
|
@@ -1297,9 +1745,9 @@ function isResolvedCrm(x) {
|
|
|
1297
1745
|
}
|
|
1298
1746
|
function defineChapter(config) {
|
|
1299
1747
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1300
|
-
const { id:
|
|
1301
|
-
if (typeof
|
|
1302
|
-
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(
|
|
1748
|
+
const { id: id4, name } = config;
|
|
1749
|
+
if (typeof id4 !== "string" || !SLUG3.test(id4)) {
|
|
1750
|
+
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1303
1751
|
}
|
|
1304
1752
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
1305
1753
|
const mode = config.mode ?? "chapter";
|
|
@@ -1382,7 +1830,7 @@ function defineChapter(config) {
|
|
|
1382
1830
|
};
|
|
1383
1831
|
const chapter = {
|
|
1384
1832
|
config,
|
|
1385
|
-
id:
|
|
1833
|
+
id: id4,
|
|
1386
1834
|
name,
|
|
1387
1835
|
mode,
|
|
1388
1836
|
brand,
|
|
@@ -1596,8 +2044,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
1596
2044
|
function byOrder(a, b) {
|
|
1597
2045
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1598
2046
|
}
|
|
1599
|
-
function resolveTiers(group,
|
|
1600
|
-
const stored =
|
|
2047
|
+
function resolveTiers(group, rows2 = []) {
|
|
2048
|
+
const stored = rows2.filter((tier) => tier.active);
|
|
1601
2049
|
if (stored.length) return [...stored].sort(byOrder);
|
|
1602
2050
|
return [{
|
|
1603
2051
|
id: "standard",
|
|
@@ -1719,10 +2167,10 @@ function planDelivery(input) {
|
|
|
1719
2167
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1720
2168
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1721
2169
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1722
|
-
const
|
|
2170
|
+
const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1723
2171
|
|
|
1724
2172
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1725
|
-
return { deliver: true, transport, to, subject, text:
|
|
2173
|
+
return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
|
|
1726
2174
|
}
|
|
1727
2175
|
|
|
1728
2176
|
// src/notify.ts
|
|
@@ -1757,9 +2205,9 @@ async function sendTemplated(deps, input) {
|
|
|
1757
2205
|
error = e instanceof Error ? e.message : String(e);
|
|
1758
2206
|
}
|
|
1759
2207
|
}
|
|
1760
|
-
const
|
|
2208
|
+
const id4 = deps.newId();
|
|
1761
2209
|
const row = {
|
|
1762
|
-
id:
|
|
2210
|
+
id: id4,
|
|
1763
2211
|
groupId: input.group.id,
|
|
1764
2212
|
to: decision.to,
|
|
1765
2213
|
template: input.template,
|
|
@@ -1773,7 +2221,7 @@ async function sendTemplated(deps, input) {
|
|
|
1773
2221
|
...messageId ? { messageId } : {},
|
|
1774
2222
|
...error ? { error } : {}
|
|
1775
2223
|
};
|
|
1776
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
2224
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id4, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
1777
2225
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
1778
2226
|
}
|
|
1779
2227
|
function emailGroupFrom(row) {
|
|
@@ -1809,8 +2257,26 @@ function findApplicationRef(obj) {
|
|
|
1809
2257
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
1810
2258
|
return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
|
|
1811
2259
|
}
|
|
2260
|
+
function findGiftRef(obj) {
|
|
2261
|
+
const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
|
|
2262
|
+
const pick = (metadata) => typeof metadata.giftIntentId === "string" ? metadata.giftIntentId : void 0;
|
|
2263
|
+
const parent = obj.parent;
|
|
2264
|
+
const subscription = obj.subscription_details ?? parent?.subscription_details;
|
|
2265
|
+
const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
|
|
2266
|
+
const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
|
|
2267
|
+
return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
|
|
2268
|
+
}
|
|
1812
2269
|
function normalizeWebhookEvent(event) {
|
|
1813
2270
|
const obj = event.data?.object ?? {};
|
|
2271
|
+
const gift = findGiftRef(obj);
|
|
2272
|
+
if (gift.giftIntentId) {
|
|
2273
|
+
if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
2274
|
+
return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
|
|
2275
|
+
}
|
|
2276
|
+
if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
|
|
2277
|
+
if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
|
|
2278
|
+
return { kind: "ignored", type: event.type };
|
|
2279
|
+
}
|
|
1814
2280
|
const ref = findApplicationRef(obj);
|
|
1815
2281
|
switch (event.type) {
|
|
1816
2282
|
case "invoice.paid": {
|
|
@@ -1844,6 +2310,112 @@ function canceledPatch() {
|
|
|
1844
2310
|
return { canceled: true };
|
|
1845
2311
|
}
|
|
1846
2312
|
|
|
2313
|
+
// src/join-continuation.ts
|
|
2314
|
+
function resolveJoinContinuation(input) {
|
|
2315
|
+
if (input.status === "approved") return "accepted";
|
|
2316
|
+
if (input.interviewWaived) {
|
|
2317
|
+
if (!input.payable) return "approve";
|
|
2318
|
+
if (input.status === input.initialStatus) {
|
|
2319
|
+
return input.subscriptionStarted ? "paymentPending" : "payment";
|
|
2320
|
+
}
|
|
2321
|
+
return "approve";
|
|
2322
|
+
}
|
|
2323
|
+
if (!input.payable) return "booking";
|
|
2324
|
+
if (input.status === input.initialStatus) {
|
|
2325
|
+
return input.subscriptionStarted ? "paymentPending" : "payment";
|
|
2326
|
+
}
|
|
2327
|
+
if (canBook(input.status, input.pipeline)) return "booking";
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// src/gift-offers.ts
|
|
2332
|
+
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2333
|
+
var MAX_GIFT_OFFERS = 20;
|
|
2334
|
+
var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2335
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2336
|
+
var CURRENCY = /^[a-z]{3}$/;
|
|
2337
|
+
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2338
|
+
var text = (value, path, errors, max) => {
|
|
2339
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2340
|
+
errors.push(`${path} is required`);
|
|
2341
|
+
return "";
|
|
2342
|
+
}
|
|
2343
|
+
if (value.trim().length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2344
|
+
return value.trim();
|
|
2345
|
+
};
|
|
2346
|
+
function validateGiftOfferCatalog(input, tierIds) {
|
|
2347
|
+
const errors = [];
|
|
2348
|
+
const root = record(input);
|
|
2349
|
+
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2350
|
+
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2351
|
+
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
2352
|
+
if (!Array.isArray(root.offers)) errors.push("document.giftOffers.offers must be an array");
|
|
2353
|
+
const rawOffers = Array.isArray(root.offers) ? root.offers : [];
|
|
2354
|
+
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
2355
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2356
|
+
const offers = [];
|
|
2357
|
+
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2358
|
+
const path = `document.giftOffers.offers[${index}]`;
|
|
2359
|
+
const item = record(raw);
|
|
2360
|
+
if (!item) {
|
|
2361
|
+
errors.push(`${path} must be an object`);
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2365
|
+
for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2366
|
+
const id4 = text(item.id, `${path}.id`, errors, 80);
|
|
2367
|
+
if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2368
|
+
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2369
|
+
ids.add(id4);
|
|
2370
|
+
const targetRaw = record(item.target);
|
|
2371
|
+
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2372
|
+
if (targetRaw?.type === "tier") {
|
|
2373
|
+
const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
2374
|
+
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
2375
|
+
target = { type: "tier", tierId };
|
|
2376
|
+
} else if (targetRaw?.type === "entitlement") {
|
|
2377
|
+
const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
2378
|
+
if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2379
|
+
target = { type: "entitlement", entitlementKey };
|
|
2380
|
+
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2381
|
+
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
2382
|
+
const stripePriceId = text(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
2383
|
+
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2384
|
+
if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
|
|
2385
|
+
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
2386
|
+
if (!CURRENCY.test(currency)) errors.push(`${path}.currency must be a lowercase ISO currency code`);
|
|
2387
|
+
const lifetime = Number(item.redemptionLifetimeHours);
|
|
2388
|
+
if (!Number.isSafeInteger(lifetime) || lifetime < 1 || lifetime > 8760) errors.push(`${path}.redemptionLifetimeHours must be an integer from 1 to 8760`);
|
|
2389
|
+
const activeFrom = item.activeFrom === void 0 ? void 0 : Number(item.activeFrom);
|
|
2390
|
+
const activeUntil = item.activeUntil === void 0 ? void 0 : Number(item.activeUntil);
|
|
2391
|
+
if (activeFrom !== void 0 && (!Number.isSafeInteger(activeFrom) || activeFrom < 1)) errors.push(`${path}.activeFrom must be epoch milliseconds`);
|
|
2392
|
+
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
2393
|
+
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
2394
|
+
offers.push({
|
|
2395
|
+
id: id4,
|
|
2396
|
+
target,
|
|
2397
|
+
billingMode: item.billingMode,
|
|
2398
|
+
stripePriceId,
|
|
2399
|
+
priceCents: Number(item.priceCents),
|
|
2400
|
+
currency,
|
|
2401
|
+
redemptionLifetimeHours: lifetime,
|
|
2402
|
+
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
2403
|
+
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
2404
|
+
title: text(item.title, `${path}.title`, errors, 160),
|
|
2405
|
+
description: text(item.description, `${path}.description`, errors, 2e3),
|
|
2406
|
+
ctaLabel: text(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
2410
|
+
}
|
|
2411
|
+
var giftOfferActive = (offer, now) => (!offer.activeFrom || offer.activeFrom <= now) && (!offer.activeUntil || offer.activeUntil > now);
|
|
2412
|
+
function publicGiftOffers(catalog, now) {
|
|
2413
|
+
return (catalog?.offers ?? []).filter((offer) => giftOfferActive(offer, now)).map(({ stripePriceId: _stripe, target: _target, ...offer }) => offer);
|
|
2414
|
+
}
|
|
2415
|
+
function findGiftOffer(catalog, offerId, now) {
|
|
2416
|
+
return catalog?.offers.find((offer) => offer.id === offerId && giftOfferActive(offer, now)) ?? null;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
1847
2419
|
// src/network.ts
|
|
1848
2420
|
var import_crm5 = require("@odla-ai/crm");
|
|
1849
2421
|
|
|
@@ -1883,34 +2455,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
1883
2455
|
if (readable && raw.length <= 64) return raw;
|
|
1884
2456
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1885
2457
|
}
|
|
1886
|
-
function normalizeSharedRecord(
|
|
1887
|
-
if ("input" in
|
|
2458
|
+
function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
|
|
2459
|
+
if ("input" in record2 && record2.version === 2) {
|
|
1888
2460
|
return {
|
|
1889
2461
|
version: 2,
|
|
1890
|
-
sourceId:
|
|
1891
|
-
sourceRecordId:
|
|
1892
|
-
type:
|
|
1893
|
-
input:
|
|
2462
|
+
sourceId: record2.source.siteId,
|
|
2463
|
+
sourceRecordId: record2.source.recordId,
|
|
2464
|
+
type: record2.type,
|
|
2465
|
+
input: record2.input
|
|
1894
2466
|
};
|
|
1895
2467
|
}
|
|
1896
|
-
if ("input" in
|
|
2468
|
+
if ("input" in record2) {
|
|
1897
2469
|
return {
|
|
1898
2470
|
version: 1,
|
|
1899
2471
|
sourceId: fallbackSourceId,
|
|
1900
|
-
sourceRecordId:
|
|
1901
|
-
type:
|
|
1902
|
-
input:
|
|
2472
|
+
sourceRecordId: record2.hubRecordId,
|
|
2473
|
+
type: record2.type,
|
|
2474
|
+
input: record2.input
|
|
1903
2475
|
};
|
|
1904
2476
|
}
|
|
1905
|
-
if ("type" in
|
|
1906
|
-
const input = { name:
|
|
2477
|
+
if ("type" in record2 && record2.type === "company") {
|
|
2478
|
+
const input = { name: record2.name };
|
|
1907
2479
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1908
|
-
if (
|
|
2480
|
+
if (record2[key]) input[key] = record2[key];
|
|
1909
2481
|
}
|
|
1910
2482
|
return {
|
|
1911
2483
|
version: 1,
|
|
1912
2484
|
sourceId: fallbackSourceId,
|
|
1913
|
-
sourceRecordId:
|
|
2485
|
+
sourceRecordId: record2.hubRecordId,
|
|
1914
2486
|
type: "company",
|
|
1915
2487
|
input
|
|
1916
2488
|
};
|
|
@@ -1918,28 +2490,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
|
1918
2490
|
return {
|
|
1919
2491
|
version: 1,
|
|
1920
2492
|
sourceId: fallbackSourceId,
|
|
1921
|
-
sourceRecordId:
|
|
2493
|
+
sourceRecordId: record2.hubRecordId,
|
|
1922
2494
|
type: "person",
|
|
1923
|
-
input: sharedPersonInput(
|
|
2495
|
+
input: sharedPersonInput(record2)
|
|
1924
2496
|
};
|
|
1925
2497
|
}
|
|
1926
|
-
function sharedRecordFromCrm(crm,
|
|
1927
|
-
if (target.fields && !target.fields[
|
|
1928
|
-
throw new Error(`${target.name} does not accept "${
|
|
2498
|
+
function sharedRecordFromCrm(crm, record2, target, sourceId) {
|
|
2499
|
+
if (target.fields && !target.fields[record2.type]) {
|
|
2500
|
+
throw new Error(`${target.name} does not accept "${record2.type}" records`);
|
|
1929
2501
|
}
|
|
1930
|
-
const def = crm.type(
|
|
1931
|
-
const fields = target.fields?.[
|
|
2502
|
+
const def = crm.type(record2.type);
|
|
2503
|
+
const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
|
|
1932
2504
|
if (!fields) {
|
|
1933
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
2505
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record2.type}" records`);
|
|
1934
2506
|
}
|
|
1935
2507
|
const nameField = def.nameField ?? "name";
|
|
1936
2508
|
const input = {};
|
|
1937
2509
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
1938
|
-
const value =
|
|
2510
|
+
const value = record2.fields?.[field];
|
|
1939
2511
|
if (value !== void 0) input[field] = value;
|
|
1940
2512
|
}
|
|
1941
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
1942
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
2513
|
+
if (input[nameField] === void 0) input[nameField] = record2.name;
|
|
2514
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
|
|
1943
2515
|
}
|
|
1944
2516
|
async function upsertPerson(deps, opts) {
|
|
1945
2517
|
const email = opts.email.toLowerCase();
|
|
@@ -1953,11 +2525,11 @@ async function upsertPerson(deps, opts) {
|
|
|
1953
2525
|
const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1954
2526
|
return { recordId: created.id };
|
|
1955
2527
|
}
|
|
1956
|
-
async function findSharedRecord(deps,
|
|
2528
|
+
async function findSharedRecord(deps, record2, tag) {
|
|
1957
2529
|
const structured = await (0, import_crm5.getRecordByOrigin)(
|
|
1958
2530
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1959
|
-
|
|
1960
|
-
|
|
2531
|
+
record2.sourceId,
|
|
2532
|
+
record2.sourceRecordId
|
|
1961
2533
|
);
|
|
1962
2534
|
if (structured) return structured;
|
|
1963
2535
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -1966,53 +2538,53 @@ async function findSharedRecord(deps, record, tag) {
|
|
|
1966
2538
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1967
2539
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1968
2540
|
}
|
|
1969
|
-
const def = deps.crm.type(
|
|
2541
|
+
const def = deps.crm.type(record2.type);
|
|
1970
2542
|
const emailField = def.emailField;
|
|
1971
|
-
if (emailField && typeof
|
|
1972
|
-
const primaryEmail =
|
|
2543
|
+
if (emailField && typeof record2.input[emailField] === "string") {
|
|
2544
|
+
const primaryEmail = record2.input[emailField].toLowerCase();
|
|
1973
2545
|
const found = await deps.db.query({
|
|
1974
|
-
crm_record: { $: { where: { type:
|
|
2546
|
+
crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
|
|
1975
2547
|
});
|
|
1976
2548
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1977
2549
|
}
|
|
1978
|
-
const domain =
|
|
2550
|
+
const domain = record2.input.domain;
|
|
1979
2551
|
const domainSlot = def.fields.domain?.slot;
|
|
1980
2552
|
if (typeof domain === "string" && domainSlot) {
|
|
1981
2553
|
const found = await deps.db.query({
|
|
1982
|
-
crm_record: { $: { where: { type:
|
|
2554
|
+
crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
|
|
1983
2555
|
});
|
|
1984
2556
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1985
2557
|
}
|
|
1986
2558
|
const nameField = def.nameField ?? "name";
|
|
1987
|
-
const name =
|
|
1988
|
-
if (
|
|
2559
|
+
const name = record2.input[nameField];
|
|
2560
|
+
if (record2.type === "company" && typeof name === "string" && name.trim()) {
|
|
1989
2561
|
const found = await deps.db.query({
|
|
1990
|
-
crm_record: { $: { where: { type:
|
|
2562
|
+
crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
|
|
1991
2563
|
});
|
|
1992
2564
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1993
2565
|
}
|
|
1994
2566
|
return void 0;
|
|
1995
2567
|
}
|
|
1996
2568
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1997
|
-
const
|
|
1998
|
-
if (!
|
|
2569
|
+
const record2 = normalizeSharedRecord(shared, options.sourceId);
|
|
2570
|
+
if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
|
|
1999
2571
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
2000
2572
|
}
|
|
2001
|
-
if (options.sourceId && options.sourceId !==
|
|
2573
|
+
if (options.sourceId && options.sourceId !== record2.sourceId) {
|
|
2002
2574
|
throw new Error("signed sender does not match payload source");
|
|
2003
2575
|
}
|
|
2004
|
-
const tag = networkSourceTag(
|
|
2576
|
+
const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
|
|
2005
2577
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
2006
|
-
const existing = await findSharedRecord(deps,
|
|
2578
|
+
const existing = await findSharedRecord(deps, record2, tag);
|
|
2007
2579
|
let recordId;
|
|
2008
2580
|
if (existing && typeof existing.id === "string") {
|
|
2009
|
-
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input:
|
|
2581
|
+
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record2.input });
|
|
2010
2582
|
recordId = existing.id;
|
|
2011
2583
|
} else {
|
|
2012
|
-
recordId = `network_${shortHash(`${
|
|
2584
|
+
recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
|
|
2013
2585
|
await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
2014
|
-
type:
|
|
2015
|
-
input:
|
|
2586
|
+
type: record2.type,
|
|
2587
|
+
input: record2.input,
|
|
2016
2588
|
mutationId: `share-create:${tag}`
|
|
2017
2589
|
});
|
|
2018
2590
|
}
|
|
@@ -2022,10 +2594,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
2022
2594
|
);
|
|
2023
2595
|
await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
|
|
2024
2596
|
recordId,
|
|
2025
|
-
sourceId:
|
|
2026
|
-
sourceRecordId:
|
|
2597
|
+
sourceId: record2.sourceId,
|
|
2598
|
+
sourceRecordId: record2.sourceRecordId,
|
|
2027
2599
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
2028
|
-
payloadVersion:
|
|
2600
|
+
payloadVersion: record2.version
|
|
2029
2601
|
});
|
|
2030
2602
|
return { recordId };
|
|
2031
2603
|
}
|
|
@@ -2241,9 +2813,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
|
|
|
2241
2813
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
2242
2814
|
if (!found.ok) return false;
|
|
2243
2815
|
const users = await found.json().catch(() => null);
|
|
2244
|
-
const
|
|
2245
|
-
if (!
|
|
2246
|
-
return updateClerkUserMetadata(secretKey,
|
|
2816
|
+
const id4 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
2817
|
+
if (!id4) return false;
|
|
2818
|
+
return updateClerkUserMetadata(secretKey, id4, input, fetchImpl);
|
|
2247
2819
|
}
|
|
2248
2820
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
2249
2821
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -2299,8 +2871,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
|
|
|
2299
2871
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
2300
2872
|
return user ? toRecord(user) : null;
|
|
2301
2873
|
}
|
|
2302
|
-
async function clerkGetUser(secretKey,
|
|
2303
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
2874
|
+
async function clerkGetUser(secretKey, id4, fetchImpl = fetch) {
|
|
2875
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id4)}`, secretKey, fetchImpl).catch(() => null);
|
|
2304
2876
|
return data ? toRecord(data) : null;
|
|
2305
2877
|
}
|
|
2306
2878
|
async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
@@ -2309,15 +2881,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
2309
2881
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
2310
2882
|
const page = Array.isArray(data) ? data : [];
|
|
2311
2883
|
for (const u of page) {
|
|
2312
|
-
const
|
|
2313
|
-
if (
|
|
2884
|
+
const record2 = toRecord(u);
|
|
2885
|
+
if (record2) out.push(record2);
|
|
2314
2886
|
}
|
|
2315
2887
|
if (page.length < PAGE) break;
|
|
2316
2888
|
}
|
|
2317
2889
|
return out;
|
|
2318
2890
|
}
|
|
2319
|
-
async function clerkSetRole(secretKey,
|
|
2320
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
2891
|
+
async function clerkSetRole(secretKey, id4, role, fetchImpl = fetch) {
|
|
2892
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id4)}/metadata`, {
|
|
2321
2893
|
method: "PATCH",
|
|
2322
2894
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
2323
2895
|
body: JSON.stringify({ public_metadata: { role } })
|
|
@@ -2693,15 +3265,15 @@ function rowToTier(row, groupId) {
|
|
|
2693
3265
|
}
|
|
2694
3266
|
async function loadTiers(db, group) {
|
|
2695
3267
|
const groupId = String(group.id ?? "");
|
|
2696
|
-
let
|
|
3268
|
+
let rows2;
|
|
2697
3269
|
try {
|
|
2698
|
-
|
|
3270
|
+
rows2 = (await db.query({
|
|
2699
3271
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
2700
3272
|
})).tiers;
|
|
2701
3273
|
} catch {
|
|
2702
|
-
|
|
3274
|
+
rows2 = void 0;
|
|
2703
3275
|
}
|
|
2704
|
-
const list = Array.isArray(
|
|
3276
|
+
const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
|
|
2705
3277
|
return resolveTiers(group, list);
|
|
2706
3278
|
}
|
|
2707
3279
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -2712,32 +3284,6 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
|
2712
3284
|
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
2713
3285
|
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
2714
3286
|
|
|
2715
|
-
// src/signup-control-canonical.ts
|
|
2716
|
-
var encoder = new TextEncoder();
|
|
2717
|
-
function canonicalValue(value) {
|
|
2718
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2719
|
-
return JSON.stringify(value);
|
|
2720
|
-
}
|
|
2721
|
-
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2722
|
-
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2723
|
-
const record = value;
|
|
2724
|
-
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
2725
|
-
const item = record[key];
|
|
2726
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2727
|
-
});
|
|
2728
|
-
return `{${entries.join(",")}}`;
|
|
2729
|
-
}
|
|
2730
|
-
function canonicalSignupControl(value) {
|
|
2731
|
-
return canonicalValue(value);
|
|
2732
|
-
}
|
|
2733
|
-
async function signupControlDigest(document) {
|
|
2734
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
2735
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2736
|
-
}
|
|
2737
|
-
async function createSignupControlEnvelope(document) {
|
|
2738
|
-
return { document, digest: await signupControlDigest(document) };
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
3287
|
// src/membership-page.ts
|
|
2742
3288
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
2743
3289
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
@@ -2778,10 +3324,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2778
3324
|
errors.push(`${path} must be a string`);
|
|
2779
3325
|
return void 0;
|
|
2780
3326
|
}
|
|
2781
|
-
const
|
|
2782
|
-
if (required && !
|
|
2783
|
-
if (
|
|
2784
|
-
return
|
|
3327
|
+
const text3 = value.trim();
|
|
3328
|
+
if (required && !text3) errors.push(`${path} is required`);
|
|
3329
|
+
if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
3330
|
+
return text3;
|
|
2785
3331
|
}
|
|
2786
3332
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2787
3333
|
const errors = [];
|
|
@@ -2830,8 +3376,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2830
3376
|
else {
|
|
2831
3377
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2832
3378
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2833
|
-
const
|
|
2834
|
-
return
|
|
3379
|
+
const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
3380
|
+
return text3 ? [text3] : [];
|
|
2835
3381
|
});
|
|
2836
3382
|
}
|
|
2837
3383
|
}
|
|
@@ -2910,7 +3456,7 @@ function membershipPageDocument(input, options = {}) {
|
|
|
2910
3456
|
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
2911
3457
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
2912
3458
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2913
|
-
var
|
|
3459
|
+
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
2914
3460
|
var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
2915
3461
|
"schemaVersion",
|
|
2916
3462
|
"revision",
|
|
@@ -2922,14 +3468,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
2922
3468
|
"application",
|
|
2923
3469
|
"policy",
|
|
2924
3470
|
"scheduling",
|
|
2925
|
-
"theme"
|
|
3471
|
+
"theme",
|
|
3472
|
+
"giftOffers"
|
|
2926
3473
|
]);
|
|
2927
3474
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
2928
3475
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2929
3476
|
function unknownKeys(value, allowed, path, errors) {
|
|
2930
3477
|
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2931
3478
|
}
|
|
2932
|
-
function
|
|
3479
|
+
function text2(value, path, errors, max = 65536) {
|
|
2933
3480
|
if (typeof value !== "string" || !value.trim()) {
|
|
2934
3481
|
errors.push(`${path} is required`);
|
|
2935
3482
|
return "";
|
|
@@ -2952,25 +3499,25 @@ function tiersOf(value, errors) {
|
|
|
2952
3499
|
return [];
|
|
2953
3500
|
}
|
|
2954
3501
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
2955
|
-
const
|
|
2956
|
-
if (
|
|
2957
|
-
if (seen.has(
|
|
2958
|
-
seen.add(
|
|
2959
|
-
const name =
|
|
3502
|
+
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
3503
|
+
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3504
|
+
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
3505
|
+
seen.add(id4);
|
|
3506
|
+
const name = text2(item.name, `${path}.name`, errors, 160);
|
|
2960
3507
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
2961
3508
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
2962
3509
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
2963
3510
|
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
2964
3511
|
}
|
|
2965
|
-
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !
|
|
3512
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
|
|
2966
3513
|
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2967
3514
|
}
|
|
2968
3515
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
2969
3516
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
2970
3517
|
}
|
|
2971
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
3518
|
+
const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
2972
3519
|
return [{
|
|
2973
|
-
id:
|
|
3520
|
+
id: id4,
|
|
2974
3521
|
name,
|
|
2975
3522
|
available: item.available === true,
|
|
2976
3523
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -3017,7 +3564,7 @@ function policyOf(value, errors) {
|
|
|
3017
3564
|
const result = {};
|
|
3018
3565
|
for (const key of allowed) {
|
|
3019
3566
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3020
|
-
result[key] =
|
|
3567
|
+
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
3021
3568
|
}
|
|
3022
3569
|
return result;
|
|
3023
3570
|
}
|
|
@@ -3055,9 +3602,9 @@ function validateSignupControlDocument(input) {
|
|
|
3055
3602
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
3056
3603
|
const source = recordOf(root.source);
|
|
3057
3604
|
const target = recordOf(root.target);
|
|
3058
|
-
const appId =
|
|
3059
|
-
const chapterId =
|
|
3060
|
-
const environment =
|
|
3605
|
+
const appId = text2(source?.appId, "document.source.appId", errors, 63);
|
|
3606
|
+
const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
3607
|
+
const environment = text2(target?.environment, "document.target.environment", errors, 63);
|
|
3061
3608
|
if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3062
3609
|
if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3063
3610
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -3078,6 +3625,8 @@ function validateSignupControlDocument(input) {
|
|
|
3078
3625
|
else scheduling = root.scheduling;
|
|
3079
3626
|
}
|
|
3080
3627
|
const theme = themeOf(root.theme, errors);
|
|
3628
|
+
const giftOffers = root.giftOffers === void 0 ? void 0 : validateGiftOfferCatalog(root.giftOffers, new Set(tiers2.map((tier) => tier.id)));
|
|
3629
|
+
if (giftOffers && !giftOffers.ok) errors.push(...giftOffers.errors);
|
|
3081
3630
|
if (errors.length || !page.ok) return { ok: false, errors };
|
|
3082
3631
|
return { ok: true, value: {
|
|
3083
3632
|
schemaVersion: 1,
|
|
@@ -3090,7 +3639,8 @@ function validateSignupControlDocument(input) {
|
|
|
3090
3639
|
...application ? { application } : {},
|
|
3091
3640
|
...policy ? { policy } : {},
|
|
3092
3641
|
...scheduling ? { scheduling } : {},
|
|
3093
|
-
...theme ? { theme } : {}
|
|
3642
|
+
...theme ? { theme } : {},
|
|
3643
|
+
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
3094
3644
|
} };
|
|
3095
3645
|
}
|
|
3096
3646
|
|
|
@@ -3231,9 +3781,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
3231
3781
|
errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
|
|
3232
3782
|
continue;
|
|
3233
3783
|
}
|
|
3234
|
-
const
|
|
3235
|
-
if (
|
|
3236
|
-
if (
|
|
3784
|
+
const text3 = (match[1] ?? "").trim();
|
|
3785
|
+
if (text3.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
|
|
3786
|
+
if (text3) benefits.push(text3);
|
|
3237
3787
|
}
|
|
3238
3788
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
3239
3789
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
@@ -3333,7 +3883,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
3333
3883
|
id: chapterId,
|
|
3334
3884
|
reason: "provider_identity",
|
|
3335
3885
|
paths: [
|
|
3336
|
-
...paidTierIds.map((
|
|
3886
|
+
...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
|
|
3337
3887
|
"groups.stripePublishableKey",
|
|
3338
3888
|
"applications.stripeCustomerId",
|
|
3339
3889
|
"applications.stripeSubscriptionId",
|
|
@@ -3375,9 +3925,9 @@ async function exportChapterPromotion(input) {
|
|
|
3375
3925
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
3376
3926
|
receiptDigests: [input.digest]
|
|
3377
3927
|
};
|
|
3378
|
-
const artifact = (kind,
|
|
3928
|
+
const artifact = (kind, id4, content) => ({
|
|
3379
3929
|
kind,
|
|
3380
|
-
id:
|
|
3930
|
+
id: id4,
|
|
3381
3931
|
sourceRevision: revision,
|
|
3382
3932
|
provenance: promotionProvenance,
|
|
3383
3933
|
content: (0, import_promotion.canonicalPromotionValue)(content)
|