@odla-ai/chapter 0.34.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/README.md +83 -2
- 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 +967 -296
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +253 -3
- package/dist/index.d.ts +253 -3
- package/dist/index.js +967 -296
- 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 +1957 -722
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +200 -3
- package/dist/worker/index.d.ts +200 -3
- package/dist/worker/index.js +1968 -733
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -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,13 +20,19 @@ 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,
|
|
29
33
|
SIGNUP_CONTROL_SCHEMA_VERSION: () => SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
34
|
+
SIGNUP_CONTROL_THEME_MARKER: () => SIGNUP_CONTROL_THEME_MARKER,
|
|
35
|
+
SIGNUP_CONTROL_THEME_STYLE_ID: () => SIGNUP_CONTROL_THEME_STYLE_ID,
|
|
30
36
|
applicantProfile: () => applicantProfile,
|
|
31
37
|
applicationBookingUpdate: () => applicationBookingUpdate,
|
|
32
38
|
applicationSummary: () => applicationSummary,
|
|
@@ -53,6 +59,7 @@ __export(index_exports, {
|
|
|
53
59
|
clerkListUsers: () => clerkListUsers,
|
|
54
60
|
clerkSetRole: () => clerkSetRole,
|
|
55
61
|
clerkUserRequest: () => clerkUserRequest,
|
|
62
|
+
createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
|
|
56
63
|
createChapterIntegration: () => createChapterIntegration,
|
|
57
64
|
createClerkInvitation: () => createClerkInvitation,
|
|
58
65
|
createClerkUser: () => createClerkUser,
|
|
@@ -63,13 +70,18 @@ __export(index_exports, {
|
|
|
63
70
|
emailGroupFrom: () => emailGroupFrom,
|
|
64
71
|
endForSlot: () => endForSlot,
|
|
65
72
|
exportChapterPromotion: () => exportChapterPromotion,
|
|
73
|
+
findAdmissionGrant: () => findAdmissionGrant,
|
|
66
74
|
findApplicationRef: () => findApplicationRef,
|
|
75
|
+
findGiftOffer: () => findGiftOffer,
|
|
67
76
|
findTier: () => findTier,
|
|
68
77
|
firstPaymentPatch: () => firstPaymentPatch,
|
|
69
78
|
formatChapterCopy: () => formatChapterCopy,
|
|
70
79
|
formationFields: () => formationFields,
|
|
71
80
|
getVaultSecret: () => getVaultSecret,
|
|
81
|
+
giftOfferActive: () => giftOfferActive,
|
|
72
82
|
hasDisclaimerAck: () => hasDisclaimerAck,
|
|
83
|
+
hydrateSignupControlTheme: () => hydrateSignupControlTheme,
|
|
84
|
+
injectSignupControlThemeHead: () => injectSignupControlThemeHead,
|
|
73
85
|
introIdempotencyKey: () => introIdempotencyKey,
|
|
74
86
|
isAdminRole: () => isAdminRole,
|
|
75
87
|
isAlreadySent: () => isAlreadySent,
|
|
@@ -79,8 +91,10 @@ __export(index_exports, {
|
|
|
79
91
|
joinConfig: () => joinConfig,
|
|
80
92
|
joinConfigTiers: () => joinConfigTiers,
|
|
81
93
|
leaderCrmConfig: () => leaderCrmConfig,
|
|
94
|
+
loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
|
|
82
95
|
loadOfferableTiers: () => loadOfferableTiers,
|
|
83
96
|
loadTiers: () => loadTiers,
|
|
97
|
+
materializeAdmissionGrants: () => materializeAdmissionGrants,
|
|
84
98
|
meetingCreateRow: () => meetingCreateRow,
|
|
85
99
|
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
86
100
|
memberApplication: () => memberApplication,
|
|
@@ -88,6 +102,7 @@ __export(index_exports, {
|
|
|
88
102
|
membershipPageDocument: () => membershipPageDocument,
|
|
89
103
|
membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
|
|
90
104
|
networkSourceTag: () => networkSourceTag,
|
|
105
|
+
normalizeAdmissionEmail: () => normalizeAdmissionEmail,
|
|
91
106
|
normalizeSharedRecord: () => normalizeSharedRecord,
|
|
92
107
|
normalizeWebhookEvent: () => normalizeWebhookEvent,
|
|
93
108
|
offerableTiers: () => offerableTiers,
|
|
@@ -96,15 +111,18 @@ __export(index_exports, {
|
|
|
96
111
|
planDelivery: () => planDelivery,
|
|
97
112
|
projectApplicant: () => projectApplicant,
|
|
98
113
|
projectSharedRecord: () => projectSharedRecord,
|
|
114
|
+
publicGiftOffers: () => publicGiftOffers,
|
|
99
115
|
reconcileMeetings: () => reconcileMeetings,
|
|
100
116
|
refundedPatch: () => refundedPatch,
|
|
101
117
|
render: () => render,
|
|
118
|
+
renderSignupControlThemeHead: () => renderSignupControlThemeHead,
|
|
102
119
|
renderSummary: () => renderSummary,
|
|
103
120
|
renderTemplateBody: () => renderTemplateBody,
|
|
104
121
|
renewalPatch: () => renewalPatch,
|
|
105
122
|
resolveApplication: () => resolveApplication,
|
|
106
123
|
resolveAuth: () => resolveAuth,
|
|
107
124
|
resolveChapterCopy: () => resolveChapterCopy,
|
|
125
|
+
resolveJoinContinuation: () => resolveJoinContinuation,
|
|
108
126
|
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
109
127
|
resolvePipeline: () => resolvePipeline,
|
|
110
128
|
resolveScheduling: () => resolveScheduling,
|
|
@@ -127,6 +145,8 @@ __export(index_exports, {
|
|
|
127
145
|
tierPayable: () => tierPayable,
|
|
128
146
|
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
129
147
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
148
|
+
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
149
|
+
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
130
150
|
validateMembershipPageDocument: () => validateMembershipPageDocument,
|
|
131
151
|
validateScheduling: () => validateScheduling,
|
|
132
152
|
validateSignupControlDocument: () => validateSignupControlDocument,
|
|
@@ -152,6 +172,213 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
152
172
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
153
173
|
}
|
|
154
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
|
+
|
|
155
382
|
// src/member.ts
|
|
156
383
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
157
384
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
@@ -203,9 +430,9 @@ function resolveApplication(a) {
|
|
|
203
430
|
validateEmail: a?.validateEmail ?? true
|
|
204
431
|
};
|
|
205
432
|
}
|
|
206
|
-
var
|
|
433
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
207
434
|
function isValidEmail(value) {
|
|
208
|
-
return typeof value === "string" &&
|
|
435
|
+
return typeof value === "string" && EMAIL_RE2.test(value);
|
|
209
436
|
}
|
|
210
437
|
function clampArray(value, max) {
|
|
211
438
|
if (!Array.isArray(value)) return value;
|
|
@@ -247,14 +474,23 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
247
474
|
if (app.requireDisclaimerAck && !acked) {
|
|
248
475
|
return { ok: false, error: "disclaimerAck is required" };
|
|
249
476
|
}
|
|
250
|
-
const
|
|
251
|
-
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 };
|
|
252
479
|
for (const f of [...app.required, ...app.optional]) {
|
|
253
480
|
if (fieldStates[f]?.visible === false) continue;
|
|
254
481
|
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
255
482
|
}
|
|
256
483
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
257
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;
|
|
258
494
|
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
259
495
|
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
260
496
|
return { ok: false, error: "tierId is not an offered tier" };
|
|
@@ -262,11 +498,20 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
262
498
|
row.tierId = fields.tierId;
|
|
263
499
|
}
|
|
264
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
|
+
}
|
|
265
510
|
const { duplicate } = await db.transact(
|
|
266
|
-
|
|
511
|
+
ops,
|
|
267
512
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
268
513
|
);
|
|
269
|
-
return { ok: true, id:
|
|
514
|
+
return { ok: true, id: id4, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
270
515
|
}
|
|
271
516
|
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
272
517
|
return {
|
|
@@ -286,93 +531,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
|
286
531
|
};
|
|
287
532
|
}
|
|
288
533
|
|
|
289
|
-
// src/schema.ts
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
var id = () => attr("string", { unique: true, indexed: true });
|
|
299
|
-
var admins = {
|
|
300
|
-
attrs: {
|
|
301
|
-
id: id(),
|
|
302
|
-
email: attr("string", { unique: true, indexed: true }),
|
|
303
|
-
name: attr("string", { optional: true }),
|
|
304
|
-
note: attr("string", { optional: true })
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
var superAdmins = {
|
|
308
|
-
attrs: {
|
|
309
|
-
id: id(),
|
|
310
|
-
email: attr("string", { unique: true, indexed: true }),
|
|
311
|
-
note: attr("string", { optional: true }),
|
|
312
|
-
createdAt: attr("number", { indexed: true })
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
var networkNotes = {
|
|
316
|
-
attrs: {
|
|
317
|
-
id: id(),
|
|
318
|
-
recordKey: attr("string", { indexed: true }),
|
|
319
|
-
targetId: attr("string", { indexed: true }),
|
|
320
|
-
recordType: attr("string", { indexed: true }),
|
|
321
|
-
recordId: attr("string", { indexed: true }),
|
|
322
|
-
body: attr("string"),
|
|
323
|
-
authorId: attr("string", { indexed: true }),
|
|
324
|
-
authorEmail: attr("string", { optional: true }),
|
|
325
|
-
createdAt: attr("number", { indexed: true })
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
var signupControls = {
|
|
329
|
-
attrs: {
|
|
330
|
-
id: id(),
|
|
331
|
-
sourceId: attr("string", { indexed: true }),
|
|
332
|
-
targetId: attr("string", { indexed: true }),
|
|
333
|
-
environment: attr("string", { indexed: true }),
|
|
334
|
-
revision: attr("number", { indexed: true }),
|
|
335
|
-
digest: attr("string", { indexed: true }),
|
|
336
|
-
document: attr("json"),
|
|
337
|
-
active: attr("boolean", { indexed: true }),
|
|
338
|
-
receivedAt: attr("number", { indexed: true })
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
var applications = {
|
|
342
|
-
attrs: {
|
|
343
|
-
id: id(),
|
|
344
|
-
firstName: attr("string"),
|
|
345
|
-
lastName: attr("string"),
|
|
346
|
-
email: attr("string", { indexed: true }),
|
|
347
|
-
referral: attr("string"),
|
|
348
|
-
referralName: attr("string", { optional: true }),
|
|
349
|
-
whoYouAre: attr("string"),
|
|
350
|
-
focus: attr("json"),
|
|
351
|
-
linkedin: attr("string", { optional: true }),
|
|
352
|
-
message: attr("string"),
|
|
353
|
-
status: attr("string", { indexed: true }),
|
|
354
|
-
createdAt: attr("number", { indexed: true }),
|
|
355
|
-
meetingAt: attr("number", { indexed: true, optional: true }),
|
|
356
|
-
meetingLink: attr("string", { optional: true }),
|
|
357
|
-
clerkUserId: attr("string", { indexed: true, optional: true }),
|
|
358
|
-
// One-time/effect-repair marker. Absent means /api/me should copy the
|
|
359
|
-
// allowlisted application snapshot into Clerk private_metadata and remove
|
|
360
|
-
// the former public_metadata copy.
|
|
361
|
-
clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
|
|
362
|
-
phone: attr("string", { optional: true }),
|
|
363
|
-
state: attr("string", { optional: true }),
|
|
364
|
-
groupId: attr("string", { indexed: true, optional: true }),
|
|
365
|
-
// The tier this application joined on. Absent on rows that predate tiers.
|
|
366
|
-
tierId: attr("string", { indexed: true, optional: true }),
|
|
367
|
-
stripeCustomerId: attr("string", { indexed: true, optional: true }),
|
|
368
|
-
stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
|
|
369
|
-
renewalAt: attr("number", { optional: true }),
|
|
370
|
-
disclaimerAckAt: attr("number", { optional: true }),
|
|
371
|
-
refundPolicyAckAt: attr("number", { optional: true }),
|
|
372
|
-
prepEmailSentAt: attr("number", { optional: true }),
|
|
373
|
-
canceled: attr("boolean", { optional: true })
|
|
374
|
-
}
|
|
375
|
-
};
|
|
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
|
+
});
|
|
376
541
|
var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
377
542
|
"firstName",
|
|
378
543
|
"lastName",
|
|
@@ -386,25 +551,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
386
551
|
"phone",
|
|
387
552
|
"state"
|
|
388
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
|
+
};
|
|
389
594
|
function applicationsFor(application) {
|
|
390
595
|
const attrs = {};
|
|
391
|
-
for (const [name, spec] of Object.entries(
|
|
392
|
-
attrs[name] = {
|
|
393
|
-
...spec,
|
|
394
|
-
optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
|
|
395
|
-
};
|
|
596
|
+
for (const [name, spec] of Object.entries(applicationAttrs)) {
|
|
597
|
+
attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
|
|
396
598
|
}
|
|
397
599
|
const apply = (field, optional) => {
|
|
398
600
|
const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
|
|
399
|
-
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
if (existing && existing.type !== "string") {
|
|
403
|
-
throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
404
|
-
}
|
|
405
|
-
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`);
|
|
406
603
|
Object.defineProperty(attrs, field, {
|
|
407
|
-
value:
|
|
604
|
+
value: existing ? { ...existing, optional } : attr("string", { optional }),
|
|
408
605
|
enumerable: true,
|
|
409
606
|
configurable: true,
|
|
410
607
|
writable: true
|
|
@@ -414,75 +611,270 @@ function applicationsFor(application) {
|
|
|
414
611
|
for (const field of application.optional) apply(field, true);
|
|
415
612
|
return { attrs };
|
|
416
613
|
}
|
|
417
|
-
|
|
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 = {
|
|
418
624
|
attrs: {
|
|
419
625
|
id: id(),
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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 })
|
|
437
645
|
}
|
|
438
646
|
};
|
|
439
|
-
var
|
|
647
|
+
var admissionGrantRevisionsEntity = {
|
|
440
648
|
attrs: {
|
|
441
649
|
id: id(),
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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 })
|
|
450
842
|
}
|
|
451
843
|
};
|
|
452
844
|
var meetings = {
|
|
453
845
|
attrs: {
|
|
454
|
-
id:
|
|
455
|
-
applicationId:
|
|
456
|
-
groupId:
|
|
457
|
-
startAt:
|
|
458
|
-
endAt:
|
|
459
|
-
timezone:
|
|
460
|
-
status:
|
|
461
|
-
googleEventId:
|
|
462
|
-
meetUrl:
|
|
463
|
-
htmlLink:
|
|
464
|
-
drift:
|
|
465
|
-
driftGoogleStartAt:
|
|
466
|
-
driftDetectedAt:
|
|
467
|
-
adoptedFromGoogleAt:
|
|
468
|
-
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 })
|
|
469
861
|
}
|
|
470
862
|
};
|
|
471
863
|
var emailLog = {
|
|
472
864
|
attrs: {
|
|
473
|
-
id:
|
|
474
|
-
groupId:
|
|
475
|
-
applicationId:
|
|
476
|
-
to:
|
|
477
|
-
template:
|
|
478
|
-
subject:
|
|
479
|
-
body:
|
|
480
|
-
transport:
|
|
481
|
-
messageId:
|
|
482
|
-
redirected:
|
|
483
|
-
dedupeKey:
|
|
484
|
-
error:
|
|
485
|
-
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 })
|
|
486
878
|
}
|
|
487
879
|
};
|
|
488
880
|
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
@@ -493,6 +885,13 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
493
885
|
entities.tiers = tiers;
|
|
494
886
|
entities.meetings = meetings;
|
|
495
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;
|
|
496
895
|
if (includeSignupControl) entities.signupControls = signupControls;
|
|
497
896
|
}
|
|
498
897
|
if (auth.source === "table") entities.admins = admins;
|
|
@@ -777,6 +1176,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
777
1176
|
overview: "Overview",
|
|
778
1177
|
billing: "Billing",
|
|
779
1178
|
people: "People",
|
|
1179
|
+
admission: "Admission",
|
|
780
1180
|
network: "Network",
|
|
781
1181
|
portfolio: "Portfolio",
|
|
782
1182
|
settings: "Settings",
|
|
@@ -887,6 +1287,26 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
887
1287
|
reschedule: "Reschedule",
|
|
888
1288
|
cancel: "Cancel"
|
|
889
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
|
+
},
|
|
890
1310
|
network: {
|
|
891
1311
|
title: "Share to a follower",
|
|
892
1312
|
allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
|
|
@@ -985,6 +1405,19 @@ var DEFAULT_JOIN_COPY = {
|
|
|
985
1405
|
label: "You're booked",
|
|
986
1406
|
calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
|
|
987
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."
|
|
988
1421
|
}
|
|
989
1422
|
};
|
|
990
1423
|
var DEFAULT_MEMBERS_COPY = {
|
|
@@ -1012,6 +1445,26 @@ var DEFAULT_MEMBERS_COPY = {
|
|
|
1012
1445
|
full: {
|
|
1013
1446
|
welcome: "Welcome back."
|
|
1014
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
|
+
},
|
|
1015
1468
|
reschedule: {
|
|
1016
1469
|
changeTime: "Change your time",
|
|
1017
1470
|
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
@@ -1127,13 +1580,13 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1127
1580
|
}
|
|
1128
1581
|
function formationFields(crm, formation) {
|
|
1129
1582
|
const def = crm.type(formation.type);
|
|
1130
|
-
return [...formation.required, ...formation.optional].map((
|
|
1131
|
-
const field = def.fields[
|
|
1583
|
+
return [...formation.required, ...formation.optional].map((id4) => {
|
|
1584
|
+
const field = def.fields[id4];
|
|
1132
1585
|
return {
|
|
1133
|
-
id:
|
|
1134
|
-
label: field.label ??
|
|
1586
|
+
id: id4,
|
|
1587
|
+
label: field.label ?? id4,
|
|
1135
1588
|
type: field.type,
|
|
1136
|
-
required: formation.required.includes(
|
|
1589
|
+
required: formation.required.includes(id4),
|
|
1137
1590
|
...field.options ? { options: field.options } : {}
|
|
1138
1591
|
};
|
|
1139
1592
|
});
|
|
@@ -1292,9 +1745,9 @@ function isResolvedCrm(x) {
|
|
|
1292
1745
|
}
|
|
1293
1746
|
function defineChapter(config) {
|
|
1294
1747
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1295
|
-
const { id:
|
|
1296
|
-
if (typeof
|
|
1297
|
-
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)}`);
|
|
1298
1751
|
}
|
|
1299
1752
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
1300
1753
|
const mode = config.mode ?? "chapter";
|
|
@@ -1377,7 +1830,7 @@ function defineChapter(config) {
|
|
|
1377
1830
|
};
|
|
1378
1831
|
const chapter = {
|
|
1379
1832
|
config,
|
|
1380
|
-
id:
|
|
1833
|
+
id: id4,
|
|
1381
1834
|
name,
|
|
1382
1835
|
mode,
|
|
1383
1836
|
brand,
|
|
@@ -1591,8 +2044,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
1591
2044
|
function byOrder(a, b) {
|
|
1592
2045
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1593
2046
|
}
|
|
1594
|
-
function resolveTiers(group,
|
|
1595
|
-
const stored =
|
|
2047
|
+
function resolveTiers(group, rows2 = []) {
|
|
2048
|
+
const stored = rows2.filter((tier) => tier.active);
|
|
1596
2049
|
if (stored.length) return [...stored].sort(byOrder);
|
|
1597
2050
|
return [{
|
|
1598
2051
|
id: "standard",
|
|
@@ -1714,10 +2167,10 @@ function planDelivery(input) {
|
|
|
1714
2167
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1715
2168
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1716
2169
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1717
|
-
const
|
|
2170
|
+
const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1718
2171
|
|
|
1719
2172
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1720
|
-
return { deliver: true, transport, to, subject, text:
|
|
2173
|
+
return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
|
|
1721
2174
|
}
|
|
1722
2175
|
|
|
1723
2176
|
// src/notify.ts
|
|
@@ -1752,9 +2205,9 @@ async function sendTemplated(deps, input) {
|
|
|
1752
2205
|
error = e instanceof Error ? e.message : String(e);
|
|
1753
2206
|
}
|
|
1754
2207
|
}
|
|
1755
|
-
const
|
|
2208
|
+
const id4 = deps.newId();
|
|
1756
2209
|
const row = {
|
|
1757
|
-
id:
|
|
2210
|
+
id: id4,
|
|
1758
2211
|
groupId: input.group.id,
|
|
1759
2212
|
to: decision.to,
|
|
1760
2213
|
template: input.template,
|
|
@@ -1768,7 +2221,7 @@ async function sendTemplated(deps, input) {
|
|
|
1768
2221
|
...messageId ? { messageId } : {},
|
|
1769
2222
|
...error ? { error } : {}
|
|
1770
2223
|
};
|
|
1771
|
-
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}` });
|
|
1772
2225
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
1773
2226
|
}
|
|
1774
2227
|
function emailGroupFrom(row) {
|
|
@@ -1804,8 +2257,26 @@ function findApplicationRef(obj) {
|
|
|
1804
2257
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
1805
2258
|
return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
|
|
1806
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
|
+
}
|
|
1807
2269
|
function normalizeWebhookEvent(event) {
|
|
1808
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
|
+
}
|
|
1809
2280
|
const ref = findApplicationRef(obj);
|
|
1810
2281
|
switch (event.type) {
|
|
1811
2282
|
case "invoice.paid": {
|
|
@@ -1839,6 +2310,112 @@ function canceledPatch() {
|
|
|
1839
2310
|
return { canceled: true };
|
|
1840
2311
|
}
|
|
1841
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
|
+
|
|
1842
2419
|
// src/network.ts
|
|
1843
2420
|
var import_crm5 = require("@odla-ai/crm");
|
|
1844
2421
|
|
|
@@ -1878,34 +2455,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
1878
2455
|
if (readable && raw.length <= 64) return raw;
|
|
1879
2456
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1880
2457
|
}
|
|
1881
|
-
function normalizeSharedRecord(
|
|
1882
|
-
if ("input" in
|
|
2458
|
+
function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
|
|
2459
|
+
if ("input" in record2 && record2.version === 2) {
|
|
1883
2460
|
return {
|
|
1884
2461
|
version: 2,
|
|
1885
|
-
sourceId:
|
|
1886
|
-
sourceRecordId:
|
|
1887
|
-
type:
|
|
1888
|
-
input:
|
|
2462
|
+
sourceId: record2.source.siteId,
|
|
2463
|
+
sourceRecordId: record2.source.recordId,
|
|
2464
|
+
type: record2.type,
|
|
2465
|
+
input: record2.input
|
|
1889
2466
|
};
|
|
1890
2467
|
}
|
|
1891
|
-
if ("input" in
|
|
2468
|
+
if ("input" in record2) {
|
|
1892
2469
|
return {
|
|
1893
2470
|
version: 1,
|
|
1894
2471
|
sourceId: fallbackSourceId,
|
|
1895
|
-
sourceRecordId:
|
|
1896
|
-
type:
|
|
1897
|
-
input:
|
|
2472
|
+
sourceRecordId: record2.hubRecordId,
|
|
2473
|
+
type: record2.type,
|
|
2474
|
+
input: record2.input
|
|
1898
2475
|
};
|
|
1899
2476
|
}
|
|
1900
|
-
if ("type" in
|
|
1901
|
-
const input = { name:
|
|
2477
|
+
if ("type" in record2 && record2.type === "company") {
|
|
2478
|
+
const input = { name: record2.name };
|
|
1902
2479
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1903
|
-
if (
|
|
2480
|
+
if (record2[key]) input[key] = record2[key];
|
|
1904
2481
|
}
|
|
1905
2482
|
return {
|
|
1906
2483
|
version: 1,
|
|
1907
2484
|
sourceId: fallbackSourceId,
|
|
1908
|
-
sourceRecordId:
|
|
2485
|
+
sourceRecordId: record2.hubRecordId,
|
|
1909
2486
|
type: "company",
|
|
1910
2487
|
input
|
|
1911
2488
|
};
|
|
@@ -1913,28 +2490,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
|
1913
2490
|
return {
|
|
1914
2491
|
version: 1,
|
|
1915
2492
|
sourceId: fallbackSourceId,
|
|
1916
|
-
sourceRecordId:
|
|
2493
|
+
sourceRecordId: record2.hubRecordId,
|
|
1917
2494
|
type: "person",
|
|
1918
|
-
input: sharedPersonInput(
|
|
2495
|
+
input: sharedPersonInput(record2)
|
|
1919
2496
|
};
|
|
1920
2497
|
}
|
|
1921
|
-
function sharedRecordFromCrm(crm,
|
|
1922
|
-
if (target.fields && !target.fields[
|
|
1923
|
-
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`);
|
|
1924
2501
|
}
|
|
1925
|
-
const def = crm.type(
|
|
1926
|
-
const fields = target.fields?.[
|
|
2502
|
+
const def = crm.type(record2.type);
|
|
2503
|
+
const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
|
|
1927
2504
|
if (!fields) {
|
|
1928
|
-
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`);
|
|
1929
2506
|
}
|
|
1930
2507
|
const nameField = def.nameField ?? "name";
|
|
1931
2508
|
const input = {};
|
|
1932
2509
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
1933
|
-
const value =
|
|
2510
|
+
const value = record2.fields?.[field];
|
|
1934
2511
|
if (value !== void 0) input[field] = value;
|
|
1935
2512
|
}
|
|
1936
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
1937
|
-
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 };
|
|
1938
2515
|
}
|
|
1939
2516
|
async function upsertPerson(deps, opts) {
|
|
1940
2517
|
const email = opts.email.toLowerCase();
|
|
@@ -1948,11 +2525,11 @@ async function upsertPerson(deps, opts) {
|
|
|
1948
2525
|
const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1949
2526
|
return { recordId: created.id };
|
|
1950
2527
|
}
|
|
1951
|
-
async function findSharedRecord(deps,
|
|
2528
|
+
async function findSharedRecord(deps, record2, tag) {
|
|
1952
2529
|
const structured = await (0, import_crm5.getRecordByOrigin)(
|
|
1953
2530
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1954
|
-
|
|
1955
|
-
|
|
2531
|
+
record2.sourceId,
|
|
2532
|
+
record2.sourceRecordId
|
|
1956
2533
|
);
|
|
1957
2534
|
if (structured) return structured;
|
|
1958
2535
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -1961,53 +2538,53 @@ async function findSharedRecord(deps, record, tag) {
|
|
|
1961
2538
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1962
2539
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1963
2540
|
}
|
|
1964
|
-
const def = deps.crm.type(
|
|
2541
|
+
const def = deps.crm.type(record2.type);
|
|
1965
2542
|
const emailField = def.emailField;
|
|
1966
|
-
if (emailField && typeof
|
|
1967
|
-
const primaryEmail =
|
|
2543
|
+
if (emailField && typeof record2.input[emailField] === "string") {
|
|
2544
|
+
const primaryEmail = record2.input[emailField].toLowerCase();
|
|
1968
2545
|
const found = await deps.db.query({
|
|
1969
|
-
crm_record: { $: { where: { type:
|
|
2546
|
+
crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
|
|
1970
2547
|
});
|
|
1971
2548
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1972
2549
|
}
|
|
1973
|
-
const domain =
|
|
2550
|
+
const domain = record2.input.domain;
|
|
1974
2551
|
const domainSlot = def.fields.domain?.slot;
|
|
1975
2552
|
if (typeof domain === "string" && domainSlot) {
|
|
1976
2553
|
const found = await deps.db.query({
|
|
1977
|
-
crm_record: { $: { where: { type:
|
|
2554
|
+
crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
|
|
1978
2555
|
});
|
|
1979
2556
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1980
2557
|
}
|
|
1981
2558
|
const nameField = def.nameField ?? "name";
|
|
1982
|
-
const name =
|
|
1983
|
-
if (
|
|
2559
|
+
const name = record2.input[nameField];
|
|
2560
|
+
if (record2.type === "company" && typeof name === "string" && name.trim()) {
|
|
1984
2561
|
const found = await deps.db.query({
|
|
1985
|
-
crm_record: { $: { where: { type:
|
|
2562
|
+
crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
|
|
1986
2563
|
});
|
|
1987
2564
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1988
2565
|
}
|
|
1989
2566
|
return void 0;
|
|
1990
2567
|
}
|
|
1991
2568
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1992
|
-
const
|
|
1993
|
-
if (!
|
|
2569
|
+
const record2 = normalizeSharedRecord(shared, options.sourceId);
|
|
2570
|
+
if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
|
|
1994
2571
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
1995
2572
|
}
|
|
1996
|
-
if (options.sourceId && options.sourceId !==
|
|
2573
|
+
if (options.sourceId && options.sourceId !== record2.sourceId) {
|
|
1997
2574
|
throw new Error("signed sender does not match payload source");
|
|
1998
2575
|
}
|
|
1999
|
-
const tag = networkSourceTag(
|
|
2576
|
+
const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
|
|
2000
2577
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
2001
|
-
const existing = await findSharedRecord(deps,
|
|
2578
|
+
const existing = await findSharedRecord(deps, record2, tag);
|
|
2002
2579
|
let recordId;
|
|
2003
2580
|
if (existing && typeof existing.id === "string") {
|
|
2004
|
-
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input:
|
|
2581
|
+
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record2.input });
|
|
2005
2582
|
recordId = existing.id;
|
|
2006
2583
|
} else {
|
|
2007
|
-
recordId = `network_${shortHash(`${
|
|
2584
|
+
recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
|
|
2008
2585
|
await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
2009
|
-
type:
|
|
2010
|
-
input:
|
|
2586
|
+
type: record2.type,
|
|
2587
|
+
input: record2.input,
|
|
2011
2588
|
mutationId: `share-create:${tag}`
|
|
2012
2589
|
});
|
|
2013
2590
|
}
|
|
@@ -2017,10 +2594,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
2017
2594
|
);
|
|
2018
2595
|
await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
|
|
2019
2596
|
recordId,
|
|
2020
|
-
sourceId:
|
|
2021
|
-
sourceRecordId:
|
|
2597
|
+
sourceId: record2.sourceId,
|
|
2598
|
+
sourceRecordId: record2.sourceRecordId,
|
|
2022
2599
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
2023
|
-
payloadVersion:
|
|
2600
|
+
payloadVersion: record2.version
|
|
2024
2601
|
});
|
|
2025
2602
|
return { recordId };
|
|
2026
2603
|
}
|
|
@@ -2236,9 +2813,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
|
|
|
2236
2813
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
2237
2814
|
if (!found.ok) return false;
|
|
2238
2815
|
const users = await found.json().catch(() => null);
|
|
2239
|
-
const
|
|
2240
|
-
if (!
|
|
2241
|
-
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);
|
|
2242
2819
|
}
|
|
2243
2820
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
2244
2821
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -2294,8 +2871,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
|
|
|
2294
2871
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
2295
2872
|
return user ? toRecord(user) : null;
|
|
2296
2873
|
}
|
|
2297
|
-
async function clerkGetUser(secretKey,
|
|
2298
|
-
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);
|
|
2299
2876
|
return data ? toRecord(data) : null;
|
|
2300
2877
|
}
|
|
2301
2878
|
async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
@@ -2304,15 +2881,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
2304
2881
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
2305
2882
|
const page = Array.isArray(data) ? data : [];
|
|
2306
2883
|
for (const u of page) {
|
|
2307
|
-
const
|
|
2308
|
-
if (
|
|
2884
|
+
const record2 = toRecord(u);
|
|
2885
|
+
if (record2) out.push(record2);
|
|
2309
2886
|
}
|
|
2310
2887
|
if (page.length < PAGE) break;
|
|
2311
2888
|
}
|
|
2312
2889
|
return out;
|
|
2313
2890
|
}
|
|
2314
|
-
async function clerkSetRole(secretKey,
|
|
2315
|
-
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`, {
|
|
2316
2893
|
method: "PATCH",
|
|
2317
2894
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
2318
2895
|
body: JSON.stringify({ public_metadata: { role } })
|
|
@@ -2688,15 +3265,15 @@ function rowToTier(row, groupId) {
|
|
|
2688
3265
|
}
|
|
2689
3266
|
async function loadTiers(db, group) {
|
|
2690
3267
|
const groupId = String(group.id ?? "");
|
|
2691
|
-
let
|
|
3268
|
+
let rows2;
|
|
2692
3269
|
try {
|
|
2693
|
-
|
|
3270
|
+
rows2 = (await db.query({
|
|
2694
3271
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
2695
3272
|
})).tiers;
|
|
2696
3273
|
} catch {
|
|
2697
|
-
|
|
3274
|
+
rows2 = void 0;
|
|
2698
3275
|
}
|
|
2699
|
-
const list = Array.isArray(
|
|
3276
|
+
const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
|
|
2700
3277
|
return resolveTiers(group, list);
|
|
2701
3278
|
}
|
|
2702
3279
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -2707,32 +3284,6 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
|
2707
3284
|
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
2708
3285
|
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
2709
3286
|
|
|
2710
|
-
// src/signup-control-canonical.ts
|
|
2711
|
-
var encoder = new TextEncoder();
|
|
2712
|
-
function canonicalValue(value) {
|
|
2713
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2714
|
-
return JSON.stringify(value);
|
|
2715
|
-
}
|
|
2716
|
-
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2717
|
-
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2718
|
-
const record = value;
|
|
2719
|
-
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
2720
|
-
const item = record[key];
|
|
2721
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2722
|
-
});
|
|
2723
|
-
return `{${entries.join(",")}}`;
|
|
2724
|
-
}
|
|
2725
|
-
function canonicalSignupControl(value) {
|
|
2726
|
-
return canonicalValue(value);
|
|
2727
|
-
}
|
|
2728
|
-
async function signupControlDigest(document) {
|
|
2729
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
2730
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2731
|
-
}
|
|
2732
|
-
async function createSignupControlEnvelope(document) {
|
|
2733
|
-
return { document, digest: await signupControlDigest(document) };
|
|
2734
|
-
}
|
|
2735
|
-
|
|
2736
3287
|
// src/membership-page.ts
|
|
2737
3288
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
2738
3289
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
@@ -2773,10 +3324,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2773
3324
|
errors.push(`${path} must be a string`);
|
|
2774
3325
|
return void 0;
|
|
2775
3326
|
}
|
|
2776
|
-
const
|
|
2777
|
-
if (required && !
|
|
2778
|
-
if (
|
|
2779
|
-
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;
|
|
2780
3331
|
}
|
|
2781
3332
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2782
3333
|
const errors = [];
|
|
@@ -2825,8 +3376,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2825
3376
|
else {
|
|
2826
3377
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2827
3378
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2828
|
-
const
|
|
2829
|
-
return
|
|
3379
|
+
const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
3380
|
+
return text3 ? [text3] : [];
|
|
2830
3381
|
});
|
|
2831
3382
|
}
|
|
2832
3383
|
}
|
|
@@ -2905,7 +3456,7 @@ function membershipPageDocument(input, options = {}) {
|
|
|
2905
3456
|
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
2906
3457
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
2907
3458
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2908
|
-
var
|
|
3459
|
+
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
2909
3460
|
var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
2910
3461
|
"schemaVersion",
|
|
2911
3462
|
"revision",
|
|
@@ -2917,14 +3468,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
2917
3468
|
"application",
|
|
2918
3469
|
"policy",
|
|
2919
3470
|
"scheduling",
|
|
2920
|
-
"theme"
|
|
3471
|
+
"theme",
|
|
3472
|
+
"giftOffers"
|
|
2921
3473
|
]);
|
|
2922
3474
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
2923
3475
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2924
3476
|
function unknownKeys(value, allowed, path, errors) {
|
|
2925
3477
|
for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2926
3478
|
}
|
|
2927
|
-
function
|
|
3479
|
+
function text2(value, path, errors, max = 65536) {
|
|
2928
3480
|
if (typeof value !== "string" || !value.trim()) {
|
|
2929
3481
|
errors.push(`${path} is required`);
|
|
2930
3482
|
return "";
|
|
@@ -2947,25 +3499,25 @@ function tiersOf(value, errors) {
|
|
|
2947
3499
|
return [];
|
|
2948
3500
|
}
|
|
2949
3501
|
unknownKeys(item, TIER_KEYS2, path, errors);
|
|
2950
|
-
const
|
|
2951
|
-
if (
|
|
2952
|
-
if (seen.has(
|
|
2953
|
-
seen.add(
|
|
2954
|
-
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);
|
|
2955
3507
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
2956
3508
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
2957
3509
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
2958
3510
|
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
2959
3511
|
}
|
|
2960
|
-
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !
|
|
3512
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
|
|
2961
3513
|
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2962
3514
|
}
|
|
2963
3515
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
2964
3516
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
2965
3517
|
}
|
|
2966
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
3518
|
+
const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
2967
3519
|
return [{
|
|
2968
|
-
id:
|
|
3520
|
+
id: id4,
|
|
2969
3521
|
name,
|
|
2970
3522
|
available: item.available === true,
|
|
2971
3523
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -3012,7 +3564,7 @@ function policyOf(value, errors) {
|
|
|
3012
3564
|
const result = {};
|
|
3013
3565
|
for (const key of allowed) {
|
|
3014
3566
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3015
|
-
result[key] =
|
|
3567
|
+
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
3016
3568
|
}
|
|
3017
3569
|
return result;
|
|
3018
3570
|
}
|
|
@@ -3026,12 +3578,18 @@ function themeOf(value, errors) {
|
|
|
3026
3578
|
}
|
|
3027
3579
|
unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
3028
3580
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
3581
|
+
let serializedLength = 0;
|
|
3029
3582
|
for (const [key, token] of Object.entries(tokens)) {
|
|
3030
|
-
if (!/^--[a-z0-9-]{
|
|
3031
|
-
|
|
3032
|
-
|
|
3583
|
+
if (!/^--ui-[a-z0-9][a-z0-9-]{0,94}$/.test(key)) {
|
|
3584
|
+
errors.push(`document.theme.tokens.${key} must be an allowlisted --ui-* custom property`);
|
|
3585
|
+
}
|
|
3586
|
+
const rendered = String(token).trim();
|
|
3587
|
+
serializedLength += key.length + rendered.length + 4;
|
|
3588
|
+
if (typeof token !== "string" && typeof token !== "number" || typeof token === "number" && !Number.isFinite(token) || !rendered || rendered.length > 1e3 || /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i.test(rendered)) {
|
|
3589
|
+
errors.push(`document.theme.tokens.${key} must be a bounded breakout-safe CSS value`);
|
|
3033
3590
|
}
|
|
3034
3591
|
}
|
|
3592
|
+
if (serializedLength > 65536) errors.push("document.theme.tokens exceeds the serialized size bound");
|
|
3035
3593
|
return { tokens };
|
|
3036
3594
|
}
|
|
3037
3595
|
function validateSignupControlDocument(input) {
|
|
@@ -3044,9 +3602,9 @@ function validateSignupControlDocument(input) {
|
|
|
3044
3602
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
3045
3603
|
const source = recordOf(root.source);
|
|
3046
3604
|
const target = recordOf(root.target);
|
|
3047
|
-
const appId =
|
|
3048
|
-
const chapterId =
|
|
3049
|
-
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);
|
|
3050
3608
|
if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3051
3609
|
if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3052
3610
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -3067,6 +3625,8 @@ function validateSignupControlDocument(input) {
|
|
|
3067
3625
|
else scheduling = root.scheduling;
|
|
3068
3626
|
}
|
|
3069
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);
|
|
3070
3630
|
if (errors.length || !page.ok) return { ok: false, errors };
|
|
3071
3631
|
return { ok: true, value: {
|
|
3072
3632
|
schemaVersion: 1,
|
|
@@ -3079,10 +3639,121 @@ function validateSignupControlDocument(input) {
|
|
|
3079
3639
|
...application ? { application } : {},
|
|
3080
3640
|
...policy ? { policy } : {},
|
|
3081
3641
|
...scheduling ? { scheduling } : {},
|
|
3082
|
-
...theme ? { theme } : {}
|
|
3642
|
+
...theme ? { theme } : {},
|
|
3643
|
+
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
3083
3644
|
} };
|
|
3084
3645
|
}
|
|
3085
3646
|
|
|
3647
|
+
// src/signup-control-theme.ts
|
|
3648
|
+
var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
|
|
3649
|
+
var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
|
|
3650
|
+
var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
|
|
3651
|
+
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
3652
|
+
var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
|
|
3653
|
+
var MAX_TOKENS = 256;
|
|
3654
|
+
var MAX_VALUE_LENGTH = 1e3;
|
|
3655
|
+
var MAX_SERIALIZED_LENGTH = 65536;
|
|
3656
|
+
function validatedTokens(input) {
|
|
3657
|
+
const entries = Object.entries(input ?? {});
|
|
3658
|
+
if (entries.length > MAX_TOKENS) {
|
|
3659
|
+
throw new TypeError(`signup-control theme supports at most ${MAX_TOKENS} tokens`);
|
|
3660
|
+
}
|
|
3661
|
+
const tokens = {};
|
|
3662
|
+
let serializedLength = 0;
|
|
3663
|
+
for (const [name, raw] of entries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
3664
|
+
if (!TOKEN_NAME.test(name)) {
|
|
3665
|
+
throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
|
|
3666
|
+
}
|
|
3667
|
+
if (typeof raw !== "string" && typeof raw !== "number" || typeof raw === "number" && !Number.isFinite(raw)) {
|
|
3668
|
+
throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
|
|
3669
|
+
}
|
|
3670
|
+
const value = String(raw).trim();
|
|
3671
|
+
if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
|
|
3672
|
+
throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
|
|
3673
|
+
}
|
|
3674
|
+
serializedLength += name.length + value.length + 4;
|
|
3675
|
+
if (serializedLength > MAX_SERIALIZED_LENGTH) {
|
|
3676
|
+
throw new TypeError("signup-control theme exceeds the serialized size bound");
|
|
3677
|
+
}
|
|
3678
|
+
tokens[name] = value;
|
|
3679
|
+
}
|
|
3680
|
+
return tokens;
|
|
3681
|
+
}
|
|
3682
|
+
function cssFor(tokens) {
|
|
3683
|
+
const declarations = Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`);
|
|
3684
|
+
return declarations.length ? `:root {
|
|
3685
|
+
${declarations.join("\n")}
|
|
3686
|
+
}` : "";
|
|
3687
|
+
}
|
|
3688
|
+
function stateFor(revision, options) {
|
|
3689
|
+
if (!revision) {
|
|
3690
|
+
return {
|
|
3691
|
+
css: cssFor(validatedTokens(options.bootstrapTokens)),
|
|
3692
|
+
metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
|
|
3696
|
+
throw new TypeError("signup-control theme revision metadata is invalid");
|
|
3697
|
+
}
|
|
3698
|
+
return {
|
|
3699
|
+
css: cssFor(validatedTokens(revision.theme?.tokens)),
|
|
3700
|
+
metadata: {
|
|
3701
|
+
source: "active",
|
|
3702
|
+
revision: revision.revision,
|
|
3703
|
+
digest: revision.digest,
|
|
3704
|
+
cacheKey: `signup-control:${revision.digest}`
|
|
3705
|
+
}
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
function attributes(metadata) {
|
|
3709
|
+
return [
|
|
3710
|
+
`id="${SIGNUP_CONTROL_THEME_STYLE_ID}"`,
|
|
3711
|
+
`data-signup-control-source="${metadata.source}"`,
|
|
3712
|
+
`data-signup-control-cache-key="${metadata.cacheKey}"`,
|
|
3713
|
+
...metadata.revision === null ? [] : [`data-signup-control-revision="${metadata.revision}"`],
|
|
3714
|
+
...metadata.digest === null ? [] : [`data-signup-control-digest="${metadata.digest}"`]
|
|
3715
|
+
].join(" ");
|
|
3716
|
+
}
|
|
3717
|
+
function renderSignupControlThemeHead(revision, options = {}) {
|
|
3718
|
+
const { css, metadata } = stateFor(revision, options);
|
|
3719
|
+
return { css, metadata, head: `<style ${attributes(metadata)}>${css}</style>` };
|
|
3720
|
+
}
|
|
3721
|
+
function injectSignupControlThemeHead(html, rendered, marker = SIGNUP_CONTROL_THEME_MARKER) {
|
|
3722
|
+
const existing = /<style\b(?=[^>]*\bid\s*=\s*["']odla-signup-control-theme["'])[^>]*>[\s\S]*?<\/style>/gi;
|
|
3723
|
+
const existingBlocks = html.match(existing) ?? [];
|
|
3724
|
+
if (existingBlocks.length > 1) {
|
|
3725
|
+
throw new TypeError("signup-control theme style block must appear at most once");
|
|
3726
|
+
}
|
|
3727
|
+
if (existingBlocks.length === 1) return html.replace(existing, rendered.head);
|
|
3728
|
+
const markerIndex = html.indexOf(marker);
|
|
3729
|
+
if (markerIndex >= 0) {
|
|
3730
|
+
if (html.indexOf(marker, markerIndex + marker.length) >= 0) {
|
|
3731
|
+
throw new TypeError("signup-control theme marker must appear exactly once");
|
|
3732
|
+
}
|
|
3733
|
+
return `${html.slice(0, markerIndex)}${rendered.head}${html.slice(markerIndex + marker.length)}`;
|
|
3734
|
+
}
|
|
3735
|
+
const closingHead = html.search(/<\/head\s*>/i);
|
|
3736
|
+
if (closingHead < 0) throw new TypeError("signup-control theme requires an HTML head");
|
|
3737
|
+
return `${html.slice(0, closingHead)}${rendered.head}${html.slice(closingHead)}`;
|
|
3738
|
+
}
|
|
3739
|
+
function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
3740
|
+
const next = stateFor(revision, options);
|
|
3741
|
+
const currentSource = target.getAttribute("data-signup-control-source");
|
|
3742
|
+
const currentRevision = target.getAttribute("data-signup-control-revision");
|
|
3743
|
+
const currentDigest = target.getAttribute("data-signup-control-digest");
|
|
3744
|
+
if (currentSource === next.metadata.source && currentRevision === (next.metadata.revision === null ? null : String(next.metadata.revision)) && currentDigest === next.metadata.digest) {
|
|
3745
|
+
return false;
|
|
3746
|
+
}
|
|
3747
|
+
target.textContent = next.css;
|
|
3748
|
+
target.setAttribute("data-signup-control-source", next.metadata.source);
|
|
3749
|
+
target.setAttribute("data-signup-control-cache-key", next.metadata.cacheKey);
|
|
3750
|
+
if (next.metadata.revision === null) target.removeAttribute("data-signup-control-revision");
|
|
3751
|
+
else target.setAttribute("data-signup-control-revision", String(next.metadata.revision));
|
|
3752
|
+
if (next.metadata.digest === null) target.removeAttribute("data-signup-control-digest");
|
|
3753
|
+
else target.setAttribute("data-signup-control-digest", next.metadata.digest);
|
|
3754
|
+
return true;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3086
3757
|
// src/membership-markdown.ts
|
|
3087
3758
|
var MAX_BENEFITS2 = 24;
|
|
3088
3759
|
var MAX_MARKDOWN2 = 65536;
|
|
@@ -3110,9 +3781,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
3110
3781
|
errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
|
|
3111
3782
|
continue;
|
|
3112
3783
|
}
|
|
3113
|
-
const
|
|
3114
|
-
if (
|
|
3115
|
-
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);
|
|
3116
3787
|
}
|
|
3117
3788
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
3118
3789
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
@@ -3212,7 +3883,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
3212
3883
|
id: chapterId,
|
|
3213
3884
|
reason: "provider_identity",
|
|
3214
3885
|
paths: [
|
|
3215
|
-
...paidTierIds.map((
|
|
3886
|
+
...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
|
|
3216
3887
|
"groups.stripePublishableKey",
|
|
3217
3888
|
"applications.stripeCustomerId",
|
|
3218
3889
|
"applications.stripeSubscriptionId",
|
|
@@ -3254,9 +3925,9 @@ async function exportChapterPromotion(input) {
|
|
|
3254
3925
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
3255
3926
|
receiptDigests: [input.digest]
|
|
3256
3927
|
};
|
|
3257
|
-
const artifact = (kind,
|
|
3928
|
+
const artifact = (kind, id4, content) => ({
|
|
3258
3929
|
kind,
|
|
3259
|
-
id:
|
|
3930
|
+
id: id4,
|
|
3260
3931
|
sourceRevision: revision,
|
|
3261
3932
|
provenance: promotionProvenance,
|
|
3262
3933
|
content: (0, import_promotion.canonicalPromotionValue)(content)
|