@odla-ai/chapter 0.35.0 → 0.37.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 +106 -0
- package/dist/{chunk-UJIVBGL5.js → chunk-BIYWPPH6.js} +535 -240
- package/dist/chunk-BIYWPPH6.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-SI3JHSEX.js} +319 -65
- package/dist/chunk-SI3JHSEX.js.map +1 -0
- package/dist/{copy-context-c1tqPXOR.d.ts → copy-context-Pbs3keLx.d.ts} +179 -29
- package/dist/index.cjs +1262 -326
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +439 -30
- package/dist/index.d.ts +439 -30
- package/dist/index.js +1262 -326
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +34 -3
- package/dist/ui/admin/index.js +10 -2
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +17 -3
- package/dist/ui/member/index.d.ts +111 -7
- package/dist/ui/member/index.js +8 -2
- package/dist/worker/index.cjs +2623 -917
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +251 -29
- package/dist/worker/index.d.ts +251 -29
- package/dist/worker/index.js +2657 -951
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/greenfield.md +27 -0
- 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,10 +20,17 @@ 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,
|
|
31
|
+
MEMBER_CONTENT_CONTROL_PATH: () => MEMBER_CONTENT_CONTROL_PATH,
|
|
32
|
+
MEMBER_CONTENT_PATH: () => MEMBER_CONTENT_PATH,
|
|
33
|
+
MEMBER_CONTENT_SCHEMA_VERSION: () => MEMBER_CONTENT_SCHEMA_VERSION,
|
|
27
34
|
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
28
35
|
SIGNUP_CONTROL_PATH: () => SIGNUP_CONTROL_PATH,
|
|
29
36
|
SIGNUP_CONTROL_SCHEMA_VERSION: () => SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
@@ -44,6 +51,7 @@ __export(index_exports, {
|
|
|
44
51
|
canChangeRole: () => canChangeRole,
|
|
45
52
|
canTransition: () => canTransition,
|
|
46
53
|
canceledPatch: () => canceledPatch,
|
|
54
|
+
canonicalMemberContent: () => canonicalMemberContent,
|
|
47
55
|
canonicalSignupControl: () => canonicalSignupControl,
|
|
48
56
|
chapterBrandFromTokens: () => chapterBrandFromTokens,
|
|
49
57
|
chapterDb: () => chapterDb,
|
|
@@ -55,9 +63,12 @@ __export(index_exports, {
|
|
|
55
63
|
clerkListUsers: () => clerkListUsers,
|
|
56
64
|
clerkSetRole: () => clerkSetRole,
|
|
57
65
|
clerkUserRequest: () => clerkUserRequest,
|
|
66
|
+
composeMemberContent: () => composeMemberContent,
|
|
67
|
+
createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
|
|
58
68
|
createChapterIntegration: () => createChapterIntegration,
|
|
59
69
|
createClerkInvitation: () => createClerkInvitation,
|
|
60
70
|
createClerkUser: () => createClerkUser,
|
|
71
|
+
createMemberContentEnvelope: () => createMemberContentEnvelope,
|
|
61
72
|
createSignupControlEnvelope: () => createSignupControlEnvelope,
|
|
62
73
|
dashboardMetricData: () => dashboardMetricData,
|
|
63
74
|
defaultCrm: () => defaultCrm,
|
|
@@ -65,12 +76,15 @@ __export(index_exports, {
|
|
|
65
76
|
emailGroupFrom: () => emailGroupFrom,
|
|
66
77
|
endForSlot: () => endForSlot,
|
|
67
78
|
exportChapterPromotion: () => exportChapterPromotion,
|
|
79
|
+
findAdmissionGrant: () => findAdmissionGrant,
|
|
68
80
|
findApplicationRef: () => findApplicationRef,
|
|
81
|
+
findGiftOffer: () => findGiftOffer,
|
|
69
82
|
findTier: () => findTier,
|
|
70
83
|
firstPaymentPatch: () => firstPaymentPatch,
|
|
71
84
|
formatChapterCopy: () => formatChapterCopy,
|
|
72
85
|
formationFields: () => formationFields,
|
|
73
86
|
getVaultSecret: () => getVaultSecret,
|
|
87
|
+
giftOfferActive: () => giftOfferActive,
|
|
74
88
|
hasDisclaimerAck: () => hasDisclaimerAck,
|
|
75
89
|
hydrateSignupControlTheme: () => hydrateSignupControlTheme,
|
|
76
90
|
injectSignupControlThemeHead: () => injectSignupControlThemeHead,
|
|
@@ -83,15 +97,20 @@ __export(index_exports, {
|
|
|
83
97
|
joinConfig: () => joinConfig,
|
|
84
98
|
joinConfigTiers: () => joinConfigTiers,
|
|
85
99
|
leaderCrmConfig: () => leaderCrmConfig,
|
|
100
|
+
loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
|
|
86
101
|
loadOfferableTiers: () => loadOfferableTiers,
|
|
87
102
|
loadTiers: () => loadTiers,
|
|
103
|
+
materializeAdmissionGrants: () => materializeAdmissionGrants,
|
|
88
104
|
meetingCreateRow: () => meetingCreateRow,
|
|
89
105
|
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
90
106
|
memberApplication: () => memberApplication,
|
|
107
|
+
memberContentDigest: () => memberContentDigest,
|
|
108
|
+
memberContentDocument: () => memberContentDocument,
|
|
91
109
|
memberSession: () => memberSession,
|
|
92
110
|
membershipPageDocument: () => membershipPageDocument,
|
|
93
111
|
membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
|
|
94
112
|
networkSourceTag: () => networkSourceTag,
|
|
113
|
+
normalizeAdmissionEmail: () => normalizeAdmissionEmail,
|
|
95
114
|
normalizeSharedRecord: () => normalizeSharedRecord,
|
|
96
115
|
normalizeWebhookEvent: () => normalizeWebhookEvent,
|
|
97
116
|
offerableTiers: () => offerableTiers,
|
|
@@ -99,7 +118,9 @@ __export(index_exports, {
|
|
|
99
118
|
personInputFromApp: () => personInputFromApp,
|
|
100
119
|
planDelivery: () => planDelivery,
|
|
101
120
|
projectApplicant: () => projectApplicant,
|
|
121
|
+
projectMemberContent: () => projectMemberContent,
|
|
102
122
|
projectSharedRecord: () => projectSharedRecord,
|
|
123
|
+
publicGiftOffers: () => publicGiftOffers,
|
|
103
124
|
reconcileMeetings: () => reconcileMeetings,
|
|
104
125
|
refundedPatch: () => refundedPatch,
|
|
105
126
|
render: () => render,
|
|
@@ -110,7 +131,9 @@ __export(index_exports, {
|
|
|
110
131
|
resolveApplication: () => resolveApplication,
|
|
111
132
|
resolveAuth: () => resolveAuth,
|
|
112
133
|
resolveChapterCopy: () => resolveChapterCopy,
|
|
134
|
+
resolveJoinContinuation: () => resolveJoinContinuation,
|
|
113
135
|
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
136
|
+
resolveMemberContent: () => resolveMemberContent,
|
|
114
137
|
resolvePipeline: () => resolvePipeline,
|
|
115
138
|
resolveScheduling: () => resolveScheduling,
|
|
116
139
|
resolveSignupControl: () => resolveSignupControl,
|
|
@@ -132,6 +155,9 @@ __export(index_exports, {
|
|
|
132
155
|
tierPayable: () => tierPayable,
|
|
133
156
|
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
134
157
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
158
|
+
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
159
|
+
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
160
|
+
validateMemberContentDocument: () => validateMemberContentDocument,
|
|
135
161
|
validateMembershipPageDocument: () => validateMembershipPageDocument,
|
|
136
162
|
validateScheduling: () => validateScheduling,
|
|
137
163
|
validateSignupControlDocument: () => validateSignupControlDocument,
|
|
@@ -157,6 +183,213 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
157
183
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
158
184
|
}
|
|
159
185
|
|
|
186
|
+
// src/admission-grants-contract.ts
|
|
187
|
+
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
188
|
+
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
189
|
+
|
|
190
|
+
// src/signup-control-canonical.ts
|
|
191
|
+
var encoder = new TextEncoder();
|
|
192
|
+
function canonicalValue(value) {
|
|
193
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
194
|
+
return JSON.stringify(value);
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
197
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
198
|
+
const record3 = value;
|
|
199
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
200
|
+
const item = record3[key];
|
|
201
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
202
|
+
});
|
|
203
|
+
return `{${entries.join(",")}}`;
|
|
204
|
+
}
|
|
205
|
+
function canonicalSignupControl(value) {
|
|
206
|
+
return canonicalValue(value);
|
|
207
|
+
}
|
|
208
|
+
async function signupControlDigest(document) {
|
|
209
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
210
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
211
|
+
}
|
|
212
|
+
async function createSignupControlEnvelope(document) {
|
|
213
|
+
return { document, digest: await signupControlDigest(document) };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/admission-grants.ts
|
|
217
|
+
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
218
|
+
var MAX_GRANTS = 2e3;
|
|
219
|
+
var MAX_NOTE = 2e3;
|
|
220
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
221
|
+
var isAdmissionEmail = (value) => EMAIL_RE.test(value);
|
|
222
|
+
function normalizeAdmissionEmail(email) {
|
|
223
|
+
return email.trim().toLowerCase();
|
|
224
|
+
}
|
|
225
|
+
function validateAdmissionGrantDocument(input) {
|
|
226
|
+
const errors = [];
|
|
227
|
+
const root = input && typeof input === "object" ? input : {};
|
|
228
|
+
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
229
|
+
const revision = Number(root.revision);
|
|
230
|
+
const generatedAt = Number(root.generatedAt);
|
|
231
|
+
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
232
|
+
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
233
|
+
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
234
|
+
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
235
|
+
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
236
|
+
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
237
|
+
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
238
|
+
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
239
|
+
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
240
|
+
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
241
|
+
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
242
|
+
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
243
|
+
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
244
|
+
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
245
|
+
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
246
|
+
const grants = [];
|
|
247
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
248
|
+
const seenEmails = /* @__PURE__ */ new Set();
|
|
249
|
+
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
250
|
+
const grant = raw && typeof raw === "object" ? raw : {};
|
|
251
|
+
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
252
|
+
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
253
|
+
const normalized = normalizeAdmissionEmail(email);
|
|
254
|
+
const decision = grant.decision;
|
|
255
|
+
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
256
|
+
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
257
|
+
if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
258
|
+
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
259
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
260
|
+
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
261
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
262
|
+
errors.push(`document.grants[${index}].decision is invalid`);
|
|
263
|
+
}
|
|
264
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
265
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
266
|
+
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
267
|
+
}
|
|
268
|
+
seenIds.add(id4);
|
|
269
|
+
seenEmails.add(normalized);
|
|
270
|
+
grants.push({
|
|
271
|
+
id: id4,
|
|
272
|
+
email,
|
|
273
|
+
decision,
|
|
274
|
+
...note ? { note } : {},
|
|
275
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (errors.length) return { ok: false, errors };
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
value: {
|
|
282
|
+
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
283
|
+
revision,
|
|
284
|
+
generatedAt,
|
|
285
|
+
source: { appId: sourceId, actor: sourceActor },
|
|
286
|
+
target: { chapterId, environment },
|
|
287
|
+
grants
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
async function createAdmissionGrantEnvelope(document) {
|
|
292
|
+
const checked = validateAdmissionGrantDocument(document);
|
|
293
|
+
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
294
|
+
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
295
|
+
}
|
|
296
|
+
var rows = (value) => Array.isArray(value) ? value : [];
|
|
297
|
+
function asGrant(row) {
|
|
298
|
+
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
299
|
+
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
300
|
+
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
301
|
+
return row;
|
|
302
|
+
}
|
|
303
|
+
async function findAdmissionGrant(db, targetId, email, now) {
|
|
304
|
+
const emailNormalized = normalizeAdmissionEmail(email);
|
|
305
|
+
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
306
|
+
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
307
|
+
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
308
|
+
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
309
|
+
)).sort((a, b) => {
|
|
310
|
+
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
311
|
+
return scope || b.createdAt - a.createdAt;
|
|
312
|
+
});
|
|
313
|
+
return active[0] ?? null;
|
|
314
|
+
}
|
|
315
|
+
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
316
|
+
const where = { sourceId, targetId, environment };
|
|
317
|
+
if (db.aggregate) {
|
|
318
|
+
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
319
|
+
}
|
|
320
|
+
const found = await db.query({ admissionGrantRevisions: {} });
|
|
321
|
+
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
322
|
+
}
|
|
323
|
+
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
324
|
+
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
325
|
+
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
326
|
+
where: { sourceId, targetId, environment },
|
|
327
|
+
order: { revision: "desc" },
|
|
328
|
+
limit: 1
|
|
329
|
+
} } });
|
|
330
|
+
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
331
|
+
return row ? row : null;
|
|
332
|
+
}
|
|
333
|
+
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
334
|
+
const { document, digest } = envelope;
|
|
335
|
+
const current = rows((await db.query({ admissionGrants: { $: {
|
|
336
|
+
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
337
|
+
limit: MAX_GRANTS
|
|
338
|
+
} } })).admissionGrants);
|
|
339
|
+
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
340
|
+
const ops = [];
|
|
341
|
+
for (const row of current) {
|
|
342
|
+
const id4 = String(row.id ?? "");
|
|
343
|
+
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
344
|
+
active: false,
|
|
345
|
+
revokedAt: receivedAt,
|
|
346
|
+
revokedBy: document.source.actor,
|
|
347
|
+
updatedAt: receivedAt,
|
|
348
|
+
updatedBy: document.source.actor
|
|
349
|
+
} });
|
|
350
|
+
}
|
|
351
|
+
for (const grant of document.grants) {
|
|
352
|
+
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
353
|
+
const existing = current.find((row) => row.id === id4);
|
|
354
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
355
|
+
id: id4,
|
|
356
|
+
sourceGrantId: grant.id,
|
|
357
|
+
sourceScope: "parent",
|
|
358
|
+
sourceId: document.source.appId,
|
|
359
|
+
targetId: document.target.chapterId,
|
|
360
|
+
email: grant.email,
|
|
361
|
+
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
362
|
+
decision: grant.decision,
|
|
363
|
+
...grant.note ? { note: grant.note } : {},
|
|
364
|
+
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
365
|
+
active: true,
|
|
366
|
+
sourceRevision: document.revision,
|
|
367
|
+
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
368
|
+
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
369
|
+
updatedAt: receivedAt,
|
|
370
|
+
updatedBy: document.source.actor
|
|
371
|
+
} });
|
|
372
|
+
const retract = [
|
|
373
|
+
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
374
|
+
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
375
|
+
];
|
|
376
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
377
|
+
}
|
|
378
|
+
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
379
|
+
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
380
|
+
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
381
|
+
id: revisionId,
|
|
382
|
+
sourceId: document.source.appId,
|
|
383
|
+
targetId: document.target.chapterId,
|
|
384
|
+
environment: document.target.environment,
|
|
385
|
+
revision: document.revision,
|
|
386
|
+
digest,
|
|
387
|
+
active: true,
|
|
388
|
+
receivedAt
|
|
389
|
+
} });
|
|
390
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
391
|
+
}
|
|
392
|
+
|
|
160
393
|
// src/member.ts
|
|
161
394
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
162
395
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
@@ -208,9 +441,9 @@ function resolveApplication(a) {
|
|
|
208
441
|
validateEmail: a?.validateEmail ?? true
|
|
209
442
|
};
|
|
210
443
|
}
|
|
211
|
-
var
|
|
444
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
212
445
|
function isValidEmail(value) {
|
|
213
|
-
return typeof value === "string" &&
|
|
446
|
+
return typeof value === "string" && EMAIL_RE2.test(value);
|
|
214
447
|
}
|
|
215
448
|
function clampArray(value, max) {
|
|
216
449
|
if (!Array.isArray(value)) return value;
|
|
@@ -222,14 +455,14 @@ function hasDisclaimerAck(fields) {
|
|
|
222
455
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
223
456
|
function applicantProfile(chapter, fields) {
|
|
224
457
|
const app = chapter.application;
|
|
225
|
-
const
|
|
458
|
+
const allowed2 = (f) => app.profileFields.includes(f);
|
|
226
459
|
const profile = {};
|
|
227
460
|
for (const f of [...app.required, ...app.optional]) {
|
|
228
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
461
|
+
if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
|
|
229
462
|
const v = fields[f];
|
|
230
463
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
231
464
|
}
|
|
232
|
-
if (fields.focus !== void 0 &&
|
|
465
|
+
if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
233
466
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
234
467
|
}
|
|
235
468
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -252,14 +485,23 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
252
485
|
if (app.requireDisclaimerAck && !acked) {
|
|
253
486
|
return { ok: false, error: "disclaimerAck is required" };
|
|
254
487
|
}
|
|
255
|
-
const
|
|
256
|
-
const row = { id:
|
|
488
|
+
const id4 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
|
|
489
|
+
const row = { id: id4, status: chapter.pipeline.initial, createdAt: opts.now };
|
|
257
490
|
for (const f of [...app.required, ...app.optional]) {
|
|
258
491
|
if (fieldStates[f]?.visible === false) continue;
|
|
259
492
|
if (typeof fields[f] === "string") row[f] = fields[f].trim();
|
|
260
493
|
}
|
|
261
494
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
262
495
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
496
|
+
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
497
|
+
row.emailNormalized = normalizeAdmissionEmail(fields.email);
|
|
498
|
+
}
|
|
499
|
+
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
500
|
+
row.admissionGrantId = opts.admissionGrant.id;
|
|
501
|
+
row.admissionSource = opts.admissionGrant.sourceScope;
|
|
502
|
+
row.interviewWaivedAt = opts.now;
|
|
503
|
+
}
|
|
504
|
+
if (opts.giftOfferId) row.giftOfferId = opts.giftOfferId;
|
|
263
505
|
if (typeof fields.tierId === "string" && fields.tierId) {
|
|
264
506
|
if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
|
|
265
507
|
return { ok: false, error: "tierId is not an offered tier" };
|
|
@@ -267,11 +509,20 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
267
509
|
row.tierId = fields.tierId;
|
|
268
510
|
}
|
|
269
511
|
if (acked) row.disclaimerAckAt = opts.now;
|
|
512
|
+
const ops = [
|
|
513
|
+
{ t: "update", ns: "applications", id: id4, attrs: row }
|
|
514
|
+
];
|
|
515
|
+
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
516
|
+
ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
|
|
517
|
+
consumedAt: opts.now,
|
|
518
|
+
consumedApplicationId: id4
|
|
519
|
+
} });
|
|
520
|
+
}
|
|
270
521
|
const { duplicate } = await db.transact(
|
|
271
|
-
|
|
522
|
+
ops,
|
|
272
523
|
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
273
524
|
);
|
|
274
|
-
return { ok: true, id:
|
|
525
|
+
return { ok: true, id: id4, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
275
526
|
}
|
|
276
527
|
function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
277
528
|
return {
|
|
@@ -291,93 +542,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
|
|
|
291
542
|
};
|
|
292
543
|
}
|
|
293
544
|
|
|
294
|
-
// src/schema.ts
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
var id = () => attr("string", { unique: true, indexed: true });
|
|
304
|
-
var admins = {
|
|
305
|
-
attrs: {
|
|
306
|
-
id: id(),
|
|
307
|
-
email: attr("string", { unique: true, indexed: true }),
|
|
308
|
-
name: attr("string", { optional: true }),
|
|
309
|
-
note: attr("string", { optional: true })
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
var superAdmins = {
|
|
313
|
-
attrs: {
|
|
314
|
-
id: id(),
|
|
315
|
-
email: attr("string", { unique: true, indexed: true }),
|
|
316
|
-
note: attr("string", { optional: true }),
|
|
317
|
-
createdAt: attr("number", { indexed: true })
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
var networkNotes = {
|
|
321
|
-
attrs: {
|
|
322
|
-
id: id(),
|
|
323
|
-
recordKey: attr("string", { indexed: true }),
|
|
324
|
-
targetId: attr("string", { indexed: true }),
|
|
325
|
-
recordType: attr("string", { indexed: true }),
|
|
326
|
-
recordId: attr("string", { indexed: true }),
|
|
327
|
-
body: attr("string"),
|
|
328
|
-
authorId: attr("string", { indexed: true }),
|
|
329
|
-
authorEmail: attr("string", { optional: true }),
|
|
330
|
-
createdAt: attr("number", { indexed: true })
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
var signupControls = {
|
|
334
|
-
attrs: {
|
|
335
|
-
id: id(),
|
|
336
|
-
sourceId: attr("string", { indexed: true }),
|
|
337
|
-
targetId: attr("string", { indexed: true }),
|
|
338
|
-
environment: attr("string", { indexed: true }),
|
|
339
|
-
revision: attr("number", { indexed: true }),
|
|
340
|
-
digest: attr("string", { indexed: true }),
|
|
341
|
-
document: attr("json"),
|
|
342
|
-
active: attr("boolean", { indexed: true }),
|
|
343
|
-
receivedAt: attr("number", { indexed: true })
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
var applications = {
|
|
347
|
-
attrs: {
|
|
348
|
-
id: id(),
|
|
349
|
-
firstName: attr("string"),
|
|
350
|
-
lastName: attr("string"),
|
|
351
|
-
email: attr("string", { indexed: true }),
|
|
352
|
-
referral: attr("string"),
|
|
353
|
-
referralName: attr("string", { optional: true }),
|
|
354
|
-
whoYouAre: attr("string"),
|
|
355
|
-
focus: attr("json"),
|
|
356
|
-
linkedin: attr("string", { optional: true }),
|
|
357
|
-
message: attr("string"),
|
|
358
|
-
status: attr("string", { indexed: true }),
|
|
359
|
-
createdAt: attr("number", { indexed: true }),
|
|
360
|
-
meetingAt: attr("number", { indexed: true, optional: true }),
|
|
361
|
-
meetingLink: attr("string", { optional: true }),
|
|
362
|
-
clerkUserId: attr("string", { indexed: true, optional: true }),
|
|
363
|
-
// One-time/effect-repair marker. Absent means /api/me should copy the
|
|
364
|
-
// allowlisted application snapshot into Clerk private_metadata and remove
|
|
365
|
-
// the former public_metadata copy.
|
|
366
|
-
clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
|
|
367
|
-
phone: attr("string", { optional: true }),
|
|
368
|
-
state: attr("string", { optional: true }),
|
|
369
|
-
groupId: attr("string", { indexed: true, optional: true }),
|
|
370
|
-
// The tier this application joined on. Absent on rows that predate tiers.
|
|
371
|
-
tierId: attr("string", { indexed: true, optional: true }),
|
|
372
|
-
stripeCustomerId: attr("string", { indexed: true, optional: true }),
|
|
373
|
-
stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
|
|
374
|
-
renewalAt: attr("number", { optional: true }),
|
|
375
|
-
disclaimerAckAt: attr("number", { optional: true }),
|
|
376
|
-
refundPolicyAckAt: attr("number", { optional: true }),
|
|
377
|
-
prepEmailSentAt: attr("number", { optional: true }),
|
|
378
|
-
canceled: attr("boolean", { optional: true })
|
|
379
|
-
}
|
|
380
|
-
};
|
|
545
|
+
// src/schema-applications.ts
|
|
546
|
+
var attr = (type, flags = {}) => ({
|
|
547
|
+
type,
|
|
548
|
+
unique: flags.unique ?? false,
|
|
549
|
+
indexed: flags.indexed ?? false,
|
|
550
|
+
optional: flags.optional ?? false
|
|
551
|
+
});
|
|
381
552
|
var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
382
553
|
"firstName",
|
|
383
554
|
"lastName",
|
|
@@ -391,25 +562,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
391
562
|
"phone",
|
|
392
563
|
"state"
|
|
393
564
|
]);
|
|
565
|
+
var applicationAttrs = {
|
|
566
|
+
id: attr("string", { unique: true, indexed: true }),
|
|
567
|
+
firstName: attr("string"),
|
|
568
|
+
lastName: attr("string"),
|
|
569
|
+
email: attr("string", { indexed: true }),
|
|
570
|
+
referral: attr("string"),
|
|
571
|
+
referralName: attr("string", { optional: true }),
|
|
572
|
+
whoYouAre: attr("string"),
|
|
573
|
+
focus: attr("json"),
|
|
574
|
+
linkedin: attr("string", { optional: true }),
|
|
575
|
+
message: attr("string"),
|
|
576
|
+
status: attr("string", { indexed: true }),
|
|
577
|
+
createdAt: attr("number", { indexed: true }),
|
|
578
|
+
meetingAt: attr("number", { indexed: true, optional: true }),
|
|
579
|
+
meetingLink: attr("string", { optional: true }),
|
|
580
|
+
clerkUserId: attr("string", { indexed: true, optional: true }),
|
|
581
|
+
clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
|
|
582
|
+
phone: attr("string", { optional: true }),
|
|
583
|
+
state: attr("string", { optional: true }),
|
|
584
|
+
groupId: attr("string", { indexed: true, optional: true }),
|
|
585
|
+
tierId: attr("string", { indexed: true, optional: true }),
|
|
586
|
+
stripeCustomerId: attr("string", { indexed: true, optional: true }),
|
|
587
|
+
stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
|
|
588
|
+
renewalAt: attr("number", { optional: true }),
|
|
589
|
+
disclaimerAckAt: attr("number", { optional: true }),
|
|
590
|
+
refundPolicyAckAt: attr("number", { optional: true }),
|
|
591
|
+
prepEmailSentAt: attr("number", { optional: true }),
|
|
592
|
+
canceled: attr("boolean", { optional: true }),
|
|
593
|
+
emailNormalized: attr("string", { indexed: true, optional: true }),
|
|
594
|
+
admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
|
|
595
|
+
admissionSource: attr("string", { indexed: true, optional: true }),
|
|
596
|
+
interviewWaivedAt: attr("number", { optional: true }),
|
|
597
|
+
giftOfferId: attr("string", { optional: true, indexed: true }),
|
|
598
|
+
giftIntentId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
599
|
+
giftClaimId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
600
|
+
giftOrderId: attr("string", { optional: true, indexed: true }),
|
|
601
|
+
giftPaidAt: attr("number", { optional: true, indexed: true }),
|
|
602
|
+
giftRevokedAt: attr("number", { optional: true, indexed: true }),
|
|
603
|
+
giftCheckoutStartedAt: attr("number", { optional: true, indexed: true })
|
|
604
|
+
};
|
|
394
605
|
function applicationsFor(application) {
|
|
395
606
|
const attrs = {};
|
|
396
|
-
for (const [name, spec] of Object.entries(
|
|
397
|
-
attrs[name] = {
|
|
398
|
-
...spec,
|
|
399
|
-
optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
|
|
400
|
-
};
|
|
607
|
+
for (const [name, spec] of Object.entries(applicationAttrs)) {
|
|
608
|
+
attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
|
|
401
609
|
}
|
|
402
610
|
const apply = (field, optional) => {
|
|
403
611
|
const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
|
|
404
|
-
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
if (existing && existing.type !== "string") {
|
|
408
|
-
throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
409
|
-
}
|
|
410
|
-
const spec = existing ? { ...existing, optional } : attr("string", { optional });
|
|
612
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
|
|
613
|
+
if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
|
|
411
614
|
Object.defineProperty(attrs, field, {
|
|
412
|
-
value:
|
|
615
|
+
value: existing ? { ...existing, optional } : attr("string", { optional }),
|
|
413
616
|
enumerable: true,
|
|
414
617
|
configurable: true,
|
|
415
618
|
writable: true
|
|
@@ -419,78 +622,297 @@ function applicationsFor(application) {
|
|
|
419
622
|
for (const field of application.optional) apply(field, true);
|
|
420
623
|
return { attrs };
|
|
421
624
|
}
|
|
422
|
-
|
|
625
|
+
|
|
626
|
+
// src/schema-admission.ts
|
|
627
|
+
var attr2 = (type, flags = {}) => ({
|
|
628
|
+
type,
|
|
629
|
+
unique: flags.unique ?? false,
|
|
630
|
+
indexed: flags.indexed ?? false,
|
|
631
|
+
optional: flags.optional ?? false
|
|
632
|
+
});
|
|
633
|
+
var id = () => attr2("string", { unique: true, indexed: true });
|
|
634
|
+
var admissionGrantsEntity = {
|
|
423
635
|
attrs: {
|
|
424
636
|
id: id(),
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
637
|
+
sourceGrantId: attr2("string", { indexed: true }),
|
|
638
|
+
sourceScope: attr2("string", { indexed: true }),
|
|
639
|
+
sourceId: attr2("string", { indexed: true }),
|
|
640
|
+
targetId: attr2("string", { indexed: true }),
|
|
641
|
+
email: attr2("string"),
|
|
642
|
+
emailNormalized: attr2("string", { indexed: true }),
|
|
643
|
+
decision: attr2("string", { indexed: true }),
|
|
644
|
+
note: attr2("string", { optional: true }),
|
|
645
|
+
active: attr2("boolean", { indexed: true }),
|
|
646
|
+
sourceRevision: attr2("number", { optional: true, indexed: true }),
|
|
647
|
+
createdAt: attr2("number", { indexed: true }),
|
|
648
|
+
createdBy: attr2("string", { optional: true, indexed: true }),
|
|
649
|
+
updatedAt: attr2("number", { optional: true, indexed: true }),
|
|
650
|
+
updatedBy: attr2("string", { optional: true, indexed: true }),
|
|
651
|
+
expiresAt: attr2("number", { optional: true, indexed: true }),
|
|
652
|
+
revokedAt: attr2("number", { optional: true, indexed: true }),
|
|
653
|
+
revokedBy: attr2("string", { optional: true, indexed: true }),
|
|
654
|
+
consumedAt: attr2("number", { optional: true, indexed: true }),
|
|
655
|
+
consumedApplicationId: attr2("string", { optional: true, indexed: true })
|
|
442
656
|
}
|
|
443
657
|
};
|
|
444
|
-
var
|
|
658
|
+
var admissionGrantRevisionsEntity = {
|
|
445
659
|
attrs: {
|
|
446
660
|
id: id(),
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
661
|
+
sourceId: attr2("string", { indexed: true }),
|
|
662
|
+
targetId: attr2("string", { indexed: true }),
|
|
663
|
+
environment: attr2("string", { indexed: true }),
|
|
664
|
+
revision: attr2("number", { indexed: true }),
|
|
665
|
+
digest: attr2("string", { indexed: true }),
|
|
666
|
+
active: attr2("boolean", { indexed: true }),
|
|
667
|
+
receivedAt: attr2("number", { indexed: true })
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// src/schema-gifts.ts
|
|
672
|
+
var attr3 = (type, flags = {}) => ({
|
|
673
|
+
type,
|
|
674
|
+
unique: flags.unique ?? false,
|
|
675
|
+
indexed: flags.indexed ?? false,
|
|
676
|
+
optional: flags.optional ?? false
|
|
677
|
+
});
|
|
678
|
+
var id2 = () => attr3("string", { unique: true, indexed: true });
|
|
679
|
+
var giftPurchaseIntentsEntity = { attrs: {
|
|
680
|
+
id: id2(),
|
|
681
|
+
chapterId: attr3("string", { indexed: true }),
|
|
682
|
+
environment: attr3("string", { indexed: true }),
|
|
683
|
+
purchaserType: attr3("string", { indexed: true }),
|
|
684
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
685
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
686
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
687
|
+
offerId: attr3("string", { indexed: true }),
|
|
688
|
+
offerSnapshot: attr3("json"),
|
|
689
|
+
status: attr3("string", { indexed: true }),
|
|
690
|
+
codeHash: attr3("string", { indexed: true }),
|
|
691
|
+
codeVersion: attr3("number"),
|
|
692
|
+
codeIssuedAt: attr3("number"),
|
|
693
|
+
providerObjectId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
694
|
+
providerCustomerId: attr3("string", { optional: true, indexed: true }),
|
|
695
|
+
checkoutStartedAt: attr3("number", { optional: true }),
|
|
696
|
+
paidAt: attr3("number", { optional: true, indexed: true }),
|
|
697
|
+
claimId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
698
|
+
createdAt: attr3("number", { indexed: true }),
|
|
699
|
+
updatedAt: attr3("number", { indexed: true })
|
|
700
|
+
} };
|
|
701
|
+
var giftOrdersEntity = { attrs: {
|
|
702
|
+
id: id2(),
|
|
703
|
+
intentId: attr3("string", { unique: true, indexed: true }),
|
|
704
|
+
chapterId: attr3("string", { indexed: true }),
|
|
705
|
+
environment: attr3("string", { indexed: true }),
|
|
706
|
+
offerId: attr3("string", { indexed: true }),
|
|
707
|
+
offerSnapshot: attr3("json"),
|
|
708
|
+
purchaserType: attr3("string", { indexed: true }),
|
|
709
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
710
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
711
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
712
|
+
providerEventId: attr3("string", { unique: true, indexed: true }),
|
|
713
|
+
providerObjectId: attr3("string", { indexed: true }),
|
|
714
|
+
amountCents: attr3("number"),
|
|
715
|
+
currency: attr3("string"),
|
|
716
|
+
billingMode: attr3("string"),
|
|
717
|
+
createdAt: attr3("number", { indexed: true })
|
|
718
|
+
} };
|
|
719
|
+
var giftClaimsEntity = { attrs: {
|
|
720
|
+
id: id2(),
|
|
721
|
+
orderId: attr3("string", { unique: true, indexed: true }),
|
|
722
|
+
intentId: attr3("string", { unique: true, indexed: true }),
|
|
723
|
+
chapterId: attr3("string", { indexed: true }),
|
|
724
|
+
offerId: attr3("string", { indexed: true }),
|
|
725
|
+
target: attr3("json"),
|
|
726
|
+
purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
|
|
727
|
+
purchaserUserId: attr3("string", { optional: true, indexed: true }),
|
|
728
|
+
purchaserEmailNormalized: attr3("string", { indexed: true }),
|
|
729
|
+
codeHash: attr3("string", { unique: true, indexed: true }),
|
|
730
|
+
codeVersion: attr3("number"),
|
|
731
|
+
rotationCount: attr3("number"),
|
|
732
|
+
codeIssuedAt: attr3("number"),
|
|
733
|
+
codeExpiresAt: attr3("number", { indexed: true }),
|
|
734
|
+
attemptCount: attr3("number"),
|
|
735
|
+
attemptWindowStartedAt: attr3("number"),
|
|
736
|
+
status: attr3("string", { indexed: true }),
|
|
737
|
+
redeemedAt: attr3("number", { optional: true, indexed: true }),
|
|
738
|
+
beneficiaryEmailNormalized: attr3("string", { optional: true, indexed: true }),
|
|
739
|
+
beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
|
|
740
|
+
applicationId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
741
|
+
entitlementId: attr3("string", { optional: true, unique: true, indexed: true }),
|
|
742
|
+
rotatedAt: attr3("number", { optional: true }),
|
|
743
|
+
revokedAt: attr3("number", { optional: true, indexed: true }),
|
|
744
|
+
revokedReason: attr3("string", { optional: true }),
|
|
745
|
+
createdAt: attr3("number", { indexed: true })
|
|
746
|
+
} };
|
|
747
|
+
var giftEntitlementsEntity = { attrs: {
|
|
748
|
+
id: id2(),
|
|
749
|
+
claimId: attr3("string", { unique: true, indexed: true }),
|
|
750
|
+
orderId: attr3("string", { indexed: true }),
|
|
751
|
+
chapterId: attr3("string", { indexed: true }),
|
|
752
|
+
entitlementKey: attr3("string", { indexed: true }),
|
|
753
|
+
beneficiaryEmailNormalized: attr3("string", { indexed: true }),
|
|
754
|
+
beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
|
|
755
|
+
active: attr3("boolean", { indexed: true }),
|
|
756
|
+
createdAt: attr3("number", { indexed: true }),
|
|
757
|
+
revokedAt: attr3("number", { optional: true })
|
|
758
|
+
} };
|
|
759
|
+
var giftRedemptionAttemptsEntity = { attrs: {
|
|
760
|
+
id: id2(),
|
|
761
|
+
chapterId: attr3("string", { indexed: true }),
|
|
762
|
+
bucket: attr3("string", { indexed: true }),
|
|
763
|
+
attemptCount: attr3("number"),
|
|
764
|
+
windowStartedAt: attr3("number", { indexed: true }),
|
|
765
|
+
updatedAt: attr3("number")
|
|
766
|
+
} };
|
|
767
|
+
|
|
768
|
+
// src/schema.ts
|
|
769
|
+
function attr4(type, flags = {}) {
|
|
770
|
+
return {
|
|
771
|
+
type,
|
|
772
|
+
unique: flags.unique ?? false,
|
|
773
|
+
indexed: flags.indexed ?? false,
|
|
774
|
+
optional: flags.optional ?? false
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
var id3 = () => attr4("string", { unique: true, indexed: true });
|
|
778
|
+
var admins = {
|
|
779
|
+
attrs: {
|
|
780
|
+
id: id3(),
|
|
781
|
+
email: attr4("string", { unique: true, indexed: true }),
|
|
782
|
+
name: attr4("string", { optional: true }),
|
|
783
|
+
note: attr4("string", { optional: true })
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
var superAdmins = {
|
|
787
|
+
attrs: {
|
|
788
|
+
id: id3(),
|
|
789
|
+
email: attr4("string", { unique: true, indexed: true }),
|
|
790
|
+
note: attr4("string", { optional: true }),
|
|
791
|
+
createdAt: attr4("number", { indexed: true })
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
var networkNotes = {
|
|
795
|
+
attrs: {
|
|
796
|
+
id: id3(),
|
|
797
|
+
recordKey: attr4("string", { indexed: true }),
|
|
798
|
+
targetId: attr4("string", { indexed: true }),
|
|
799
|
+
recordType: attr4("string", { indexed: true }),
|
|
800
|
+
recordId: attr4("string", { indexed: true }),
|
|
801
|
+
body: attr4("string"),
|
|
802
|
+
authorId: attr4("string", { indexed: true }),
|
|
803
|
+
authorEmail: attr4("string", { optional: true }),
|
|
804
|
+
createdAt: attr4("number", { indexed: true })
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
var signupControls = {
|
|
808
|
+
attrs: {
|
|
809
|
+
id: id3(),
|
|
810
|
+
sourceId: attr4("string", { indexed: true }),
|
|
811
|
+
targetId: attr4("string", { indexed: true }),
|
|
812
|
+
environment: attr4("string", { indexed: true }),
|
|
813
|
+
revision: attr4("number", { indexed: true }),
|
|
814
|
+
digest: attr4("string", { indexed: true }),
|
|
815
|
+
document: attr4("json"),
|
|
816
|
+
active: attr4("boolean", { indexed: true }),
|
|
817
|
+
receivedAt: attr4("number", { indexed: true })
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
var memberContentControls = {
|
|
821
|
+
attrs: {
|
|
822
|
+
id: id3(),
|
|
823
|
+
sourceId: attr4("string", { indexed: true }),
|
|
824
|
+
targetId: attr4("string", { indexed: true }),
|
|
825
|
+
environment: attr4("string", { indexed: true }),
|
|
826
|
+
revision: attr4("number", { indexed: true }),
|
|
827
|
+
digest: attr4("string", { indexed: true }),
|
|
828
|
+
document: attr4("json"),
|
|
829
|
+
active: attr4("boolean", { indexed: true }),
|
|
830
|
+
receivedAt: attr4("number", { indexed: true })
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
var memberContentLocal = {
|
|
834
|
+
attrs: {
|
|
835
|
+
id: id3(),
|
|
836
|
+
chapterId: attr4("string", { indexed: true }),
|
|
837
|
+
environment: attr4("string", { indexed: true }),
|
|
838
|
+
revision: attr4("number", { indexed: true }),
|
|
839
|
+
document: attr4("json"),
|
|
840
|
+
updatedAt: attr4("number", { indexed: true }),
|
|
841
|
+
updatedBy: attr4("string", { optional: true })
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
var groups = {
|
|
845
|
+
attrs: {
|
|
846
|
+
id: id3(),
|
|
847
|
+
name: attr4("string"),
|
|
848
|
+
standardPriceCents: attr4("number"),
|
|
849
|
+
foundingDiscountCents: attr4("number"),
|
|
850
|
+
stripePriceId: attr4("string", { optional: true }),
|
|
851
|
+
stripePublishableKey: attr4("string", { optional: true }),
|
|
852
|
+
notificationEmail: attr4("string"),
|
|
853
|
+
replyTo: attr4("string"),
|
|
854
|
+
debugEmail: attr4("string", { optional: true }),
|
|
855
|
+
calendarLink: attr4("string", { optional: true }),
|
|
856
|
+
disclaimerText: attr4("string"),
|
|
857
|
+
refundPolicyText: attr4("string"),
|
|
858
|
+
trustCopy: attr4("string"),
|
|
859
|
+
commitmentText: attr4("string", { optional: true }),
|
|
860
|
+
normsText: attr4("string", { optional: true }),
|
|
861
|
+
emailTemplates: attr4("json"),
|
|
862
|
+
schedulingJson: attr4("json", { optional: true }),
|
|
863
|
+
createdAt: attr4("number", { indexed: true })
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
var tiers = {
|
|
867
|
+
attrs: {
|
|
868
|
+
id: id3(),
|
|
869
|
+
groupId: attr4("string", { indexed: true }),
|
|
870
|
+
name: attr4("string"),
|
|
871
|
+
priceCents: attr4("number"),
|
|
872
|
+
stripePriceId: attr4("string", { optional: true }),
|
|
873
|
+
blurb: attr4("string", { optional: true }),
|
|
874
|
+
sortOrder: attr4("number", { indexed: true }),
|
|
875
|
+
active: attr4("boolean", { indexed: true }),
|
|
876
|
+
createdAt: attr4("number", { indexed: true })
|
|
455
877
|
}
|
|
456
878
|
};
|
|
457
879
|
var meetings = {
|
|
458
880
|
attrs: {
|
|
459
|
-
id:
|
|
460
|
-
applicationId:
|
|
461
|
-
groupId:
|
|
462
|
-
startAt:
|
|
463
|
-
endAt:
|
|
464
|
-
timezone:
|
|
465
|
-
status:
|
|
466
|
-
googleEventId:
|
|
467
|
-
meetUrl:
|
|
468
|
-
htmlLink:
|
|
469
|
-
drift:
|
|
470
|
-
driftGoogleStartAt:
|
|
471
|
-
driftDetectedAt:
|
|
472
|
-
adoptedFromGoogleAt:
|
|
473
|
-
createdAt:
|
|
881
|
+
id: id3(),
|
|
882
|
+
applicationId: attr4("string", { indexed: true }),
|
|
883
|
+
groupId: attr4("string", { indexed: true }),
|
|
884
|
+
startAt: attr4("number", { indexed: true }),
|
|
885
|
+
endAt: attr4("number"),
|
|
886
|
+
timezone: attr4("string"),
|
|
887
|
+
status: attr4("string", { indexed: true }),
|
|
888
|
+
googleEventId: attr4("string", { indexed: true, optional: true }),
|
|
889
|
+
meetUrl: attr4("string", { optional: true }),
|
|
890
|
+
htmlLink: attr4("string", { optional: true }),
|
|
891
|
+
drift: attr4("string", { indexed: true, optional: true }),
|
|
892
|
+
driftGoogleStartAt: attr4("number", { optional: true }),
|
|
893
|
+
driftDetectedAt: attr4("number", { optional: true }),
|
|
894
|
+
adoptedFromGoogleAt: attr4("number", { optional: true }),
|
|
895
|
+
createdAt: attr4("number", { indexed: true })
|
|
474
896
|
}
|
|
475
897
|
};
|
|
476
898
|
var emailLog = {
|
|
477
899
|
attrs: {
|
|
478
|
-
id:
|
|
479
|
-
groupId:
|
|
480
|
-
applicationId:
|
|
481
|
-
to:
|
|
482
|
-
template:
|
|
483
|
-
subject:
|
|
484
|
-
body:
|
|
485
|
-
transport:
|
|
486
|
-
messageId:
|
|
487
|
-
redirected:
|
|
488
|
-
dedupeKey:
|
|
489
|
-
error:
|
|
490
|
-
sentAt:
|
|
900
|
+
id: id3(),
|
|
901
|
+
groupId: attr4("string", { indexed: true }),
|
|
902
|
+
applicationId: attr4("string", { indexed: true, optional: true }),
|
|
903
|
+
to: attr4("string", { indexed: true }),
|
|
904
|
+
template: attr4("string", { indexed: true }),
|
|
905
|
+
subject: attr4("string"),
|
|
906
|
+
body: attr4("string", { optional: true }),
|
|
907
|
+
transport: attr4("string"),
|
|
908
|
+
messageId: attr4("string", { optional: true }),
|
|
909
|
+
redirected: attr4("boolean", { optional: true }),
|
|
910
|
+
dedupeKey: attr4("string", { indexed: true, optional: true }),
|
|
911
|
+
error: attr4("string", { optional: true }),
|
|
912
|
+
sentAt: attr4("number", { indexed: true })
|
|
491
913
|
}
|
|
492
914
|
};
|
|
493
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
915
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
|
|
494
916
|
const entities = {};
|
|
495
917
|
if (mode === "chapter") {
|
|
496
918
|
entities.applications = applicationsFor(application);
|
|
@@ -498,7 +920,18 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
498
920
|
entities.tiers = tiers;
|
|
499
921
|
entities.meetings = meetings;
|
|
500
922
|
entities.emailLog = emailLog;
|
|
923
|
+
entities.admissionGrants = admissionGrantsEntity;
|
|
924
|
+
entities.admissionGrantRevisions = admissionGrantRevisionsEntity;
|
|
925
|
+
entities.giftPurchaseIntents = giftPurchaseIntentsEntity;
|
|
926
|
+
entities.giftOrders = giftOrdersEntity;
|
|
927
|
+
entities.giftClaims = giftClaimsEntity;
|
|
928
|
+
entities.giftEntitlements = giftEntitlementsEntity;
|
|
929
|
+
entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
|
|
501
930
|
if (includeSignupControl) entities.signupControls = signupControls;
|
|
931
|
+
if (includeMemberContent) {
|
|
932
|
+
entities.memberContentControls = memberContentControls;
|
|
933
|
+
entities.memberContentLocal = memberContentLocal;
|
|
934
|
+
}
|
|
502
935
|
}
|
|
503
936
|
if (auth.source === "table") entities.admins = admins;
|
|
504
937
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
@@ -782,6 +1215,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
782
1215
|
overview: "Overview",
|
|
783
1216
|
billing: "Billing",
|
|
784
1217
|
people: "People",
|
|
1218
|
+
admission: "Admission",
|
|
785
1219
|
network: "Network",
|
|
786
1220
|
portfolio: "Portfolio",
|
|
787
1221
|
settings: "Settings",
|
|
@@ -892,6 +1326,26 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
892
1326
|
reschedule: "Reschedule",
|
|
893
1327
|
cancel: "Cancel"
|
|
894
1328
|
},
|
|
1329
|
+
admission: {
|
|
1330
|
+
title: "Interview decisions",
|
|
1331
|
+
description: "Local decisions override Parent decisions. Requiring an interview locally suppresses a Parent waiver for the same email.",
|
|
1332
|
+
email: "Email",
|
|
1333
|
+
decision: "Decision",
|
|
1334
|
+
expires: "Expires",
|
|
1335
|
+
note: "Internal note",
|
|
1336
|
+
waive: "Waive interview",
|
|
1337
|
+
require: "Require interview",
|
|
1338
|
+
add: "Add decision",
|
|
1339
|
+
ledger: "Effective grant ledger",
|
|
1340
|
+
loading: "Loading\u2026",
|
|
1341
|
+
consumed: "consumed",
|
|
1342
|
+
revoked: "revoked",
|
|
1343
|
+
expired: "expired",
|
|
1344
|
+
active: "active",
|
|
1345
|
+
local: "local",
|
|
1346
|
+
parent: "Parent",
|
|
1347
|
+
revoke: "Revoke"
|
|
1348
|
+
},
|
|
895
1349
|
network: {
|
|
896
1350
|
title: "Share to a follower",
|
|
897
1351
|
allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
|
|
@@ -990,6 +1444,19 @@ var DEFAULT_JOIN_COPY = {
|
|
|
990
1444
|
label: "You're booked",
|
|
991
1445
|
calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
|
|
992
1446
|
memberArea: "Go to your member area"
|
|
1447
|
+
},
|
|
1448
|
+
accepted: {
|
|
1449
|
+
label: "You're approved",
|
|
1450
|
+
body: "Your membership is approved. No introduction call is required."
|
|
1451
|
+
},
|
|
1452
|
+
gift: {
|
|
1453
|
+
chooseSelf: "Buy membership for myself",
|
|
1454
|
+
optionsLabel: "Or give access to someone else",
|
|
1455
|
+
label: "Your gift code",
|
|
1456
|
+
body: "Share this one-time code with the recipient. It becomes active after payment is confirmed.",
|
|
1457
|
+
save: "Save it now. For security, this code is shown only once.",
|
|
1458
|
+
rotate: "Generate a new code",
|
|
1459
|
+
rotateFailed: "A new code could not be generated."
|
|
993
1460
|
}
|
|
994
1461
|
};
|
|
995
1462
|
var DEFAULT_MEMBERS_COPY = {
|
|
@@ -1017,6 +1484,26 @@ var DEFAULT_MEMBERS_COPY = {
|
|
|
1017
1484
|
full: {
|
|
1018
1485
|
welcome: "Welcome back."
|
|
1019
1486
|
},
|
|
1487
|
+
gifts: {
|
|
1488
|
+
title: "Give access",
|
|
1489
|
+
body: "Purchase access for someone else and share the one-time claim code.",
|
|
1490
|
+
noneAvailable: "No gift options are available right now.",
|
|
1491
|
+
paid: "Payment submitted. Save the code below while it is visible.",
|
|
1492
|
+
saveCode: "This code is shown once. Save it before leaving this page.",
|
|
1493
|
+
purchases: "Your gifts",
|
|
1494
|
+
noPurchases: "No gift purchases yet.",
|
|
1495
|
+
pending: "Pending",
|
|
1496
|
+
redeemed: "Redeemed",
|
|
1497
|
+
expired: "Expired",
|
|
1498
|
+
revoked: "Revoked",
|
|
1499
|
+
rotate: "Rotate code",
|
|
1500
|
+
rotated: "A new code was generated. Save it now.",
|
|
1501
|
+
rotateFailed: "The code could not be rotated.",
|
|
1502
|
+
redeemTitle: "Redeem a gift",
|
|
1503
|
+
code: "One-time code",
|
|
1504
|
+
redeem: "Redeem",
|
|
1505
|
+
redeemFailed: "The code is invalid or unavailable."
|
|
1506
|
+
},
|
|
1020
1507
|
reschedule: {
|
|
1021
1508
|
changeTime: "Change your time",
|
|
1022
1509
|
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
@@ -1132,13 +1619,13 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1132
1619
|
}
|
|
1133
1620
|
function formationFields(crm, formation) {
|
|
1134
1621
|
const def = crm.type(formation.type);
|
|
1135
|
-
return [...formation.required, ...formation.optional].map((
|
|
1136
|
-
const field = def.fields[
|
|
1622
|
+
return [...formation.required, ...formation.optional].map((id4) => {
|
|
1623
|
+
const field = def.fields[id4];
|
|
1137
1624
|
return {
|
|
1138
|
-
id:
|
|
1139
|
-
label: field.label ??
|
|
1625
|
+
id: id4,
|
|
1626
|
+
label: field.label ?? id4,
|
|
1140
1627
|
type: field.type,
|
|
1141
|
-
required: formation.required.includes(
|
|
1628
|
+
required: formation.required.includes(id4),
|
|
1142
1629
|
...field.options ? { options: field.options } : {}
|
|
1143
1630
|
};
|
|
1144
1631
|
});
|
|
@@ -1290,16 +1777,36 @@ function resolveSignupControl(input) {
|
|
|
1290
1777
|
return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
|
|
1291
1778
|
}
|
|
1292
1779
|
|
|
1293
|
-
// src/config.ts
|
|
1780
|
+
// src/config-member-content.ts
|
|
1294
1781
|
var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1782
|
+
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1783
|
+
function resolveMemberContent(input) {
|
|
1784
|
+
if (!input) return null;
|
|
1785
|
+
const parent = input.parent;
|
|
1786
|
+
if (parent && !SLUG3.test(parent.sourceId)) {
|
|
1787
|
+
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1788
|
+
}
|
|
1789
|
+
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
1790
|
+
if (parent && !SECRET_NAME2.test(secretName)) {
|
|
1791
|
+
throw new Error("defineChapter.memberContent.parent.secretName: must be a vault-key identifier");
|
|
1792
|
+
}
|
|
1793
|
+
return {
|
|
1794
|
+
enabled: true,
|
|
1795
|
+
localAuthoring: input.localAuthoring !== false,
|
|
1796
|
+
parent: parent ? { sourceId: parent.sourceId, secretName } : null
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// src/config.ts
|
|
1801
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1295
1802
|
function isResolvedCrm(x) {
|
|
1296
1803
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1297
1804
|
}
|
|
1298
1805
|
function defineChapter(config) {
|
|
1299
1806
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1300
|
-
const { id:
|
|
1301
|
-
if (typeof
|
|
1302
|
-
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(
|
|
1807
|
+
const { id: id4, name } = config;
|
|
1808
|
+
if (typeof id4 !== "string" || !SLUG4.test(id4)) {
|
|
1809
|
+
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1303
1810
|
}
|
|
1304
1811
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
1305
1812
|
const mode = config.mode ?? "chapter";
|
|
@@ -1323,16 +1830,21 @@ function defineChapter(config) {
|
|
|
1323
1830
|
const copy = resolveChapterCopy(config.copy);
|
|
1324
1831
|
const network = resolveNetwork(config, crm);
|
|
1325
1832
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1833
|
+
const memberContent = resolveMemberContent(config.memberContent);
|
|
1326
1834
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1327
1835
|
if (signupControl && mode !== "chapter") {
|
|
1328
1836
|
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
1329
1837
|
}
|
|
1838
|
+
if (memberContent && mode !== "chapter") {
|
|
1839
|
+
throw new Error("defineChapter.memberContent: only chapter-mode sites can serve member content");
|
|
1840
|
+
}
|
|
1330
1841
|
const { schema, rules } = chapterDb(
|
|
1331
1842
|
mode,
|
|
1332
1843
|
auth,
|
|
1333
1844
|
network.targets.length > 0,
|
|
1334
1845
|
application,
|
|
1335
|
-
Boolean(signupControl)
|
|
1846
|
+
Boolean(signupControl),
|
|
1847
|
+
Boolean(memberContent)
|
|
1336
1848
|
);
|
|
1337
1849
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1338
1850
|
const account = config.account ?? "none";
|
|
@@ -1382,13 +1894,14 @@ function defineChapter(config) {
|
|
|
1382
1894
|
};
|
|
1383
1895
|
const chapter = {
|
|
1384
1896
|
config,
|
|
1385
|
-
id:
|
|
1897
|
+
id: id4,
|
|
1386
1898
|
name,
|
|
1387
1899
|
mode,
|
|
1388
1900
|
brand,
|
|
1389
1901
|
copy,
|
|
1390
1902
|
network,
|
|
1391
1903
|
signupControl,
|
|
1904
|
+
memberContent,
|
|
1392
1905
|
formation,
|
|
1393
1906
|
crm,
|
|
1394
1907
|
auth,
|
|
@@ -1596,8 +2109,8 @@ function tierPayable(tier, group, hasSecretKey) {
|
|
|
1596
2109
|
function byOrder(a, b) {
|
|
1597
2110
|
return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
|
|
1598
2111
|
}
|
|
1599
|
-
function resolveTiers(group,
|
|
1600
|
-
const stored =
|
|
2112
|
+
function resolveTiers(group, rows2 = []) {
|
|
2113
|
+
const stored = rows2.filter((tier) => tier.active);
|
|
1601
2114
|
if (stored.length) return [...stored].sort(byOrder);
|
|
1602
2115
|
return [{
|
|
1603
2116
|
id: "standard",
|
|
@@ -1683,12 +2196,332 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1683
2196
|
}] : [],
|
|
1684
2197
|
// Formation metadata is public only when the host explicitly enables
|
|
1685
2198
|
// the bounded intake contract.
|
|
1686
|
-
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
2199
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
|
|
2200
|
+
...chapter.memberContent ? [
|
|
2201
|
+
{ path: "/api/member-content", expectedStatus: 401 },
|
|
2202
|
+
{ path: "/api/admin/member-content", expectedStatus: 401 }
|
|
2203
|
+
] : []
|
|
1687
2204
|
],
|
|
1688
2205
|
runbooks
|
|
1689
2206
|
};
|
|
1690
2207
|
}
|
|
1691
2208
|
|
|
2209
|
+
// src/member-content-validation-helpers.ts
|
|
2210
|
+
var ID2 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2211
|
+
var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2212
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2213
|
+
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2214
|
+
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
2215
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
2216
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
2217
|
+
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
2218
|
+
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
2219
|
+
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
2220
|
+
var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
|
|
2221
|
+
var MAX_PAGES = 100;
|
|
2222
|
+
var MAX_MARKDOWN = 65536;
|
|
2223
|
+
var MAX_TEXT = 240;
|
|
2224
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2225
|
+
function unknownKeys(value, allowed2, path, errors) {
|
|
2226
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2227
|
+
}
|
|
2228
|
+
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
2229
|
+
if (value === void 0) {
|
|
2230
|
+
if (required) errors.push(`${path} is required`);
|
|
2231
|
+
return void 0;
|
|
2232
|
+
}
|
|
2233
|
+
if (typeof value !== "string") {
|
|
2234
|
+
errors.push(`${path} must be a string`);
|
|
2235
|
+
return void 0;
|
|
2236
|
+
}
|
|
2237
|
+
const valueText = value.trim();
|
|
2238
|
+
if (required && !valueText) errors.push(`${path} is required`);
|
|
2239
|
+
if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2240
|
+
return valueText;
|
|
2241
|
+
}
|
|
2242
|
+
function offerValue(value, path, errors, tiers2, audience) {
|
|
2243
|
+
const raw = record(value);
|
|
2244
|
+
if (!raw) {
|
|
2245
|
+
errors.push(`${path} must be an object`);
|
|
2246
|
+
return void 0;
|
|
2247
|
+
}
|
|
2248
|
+
unknownKeys(raw, OFFER_KEYS, path, errors);
|
|
2249
|
+
const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2250
|
+
if (tierId && !ID2.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2251
|
+
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2252
|
+
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2253
|
+
errors.push(`${path}.tierId must grant access to this page`);
|
|
2254
|
+
}
|
|
2255
|
+
const label = textValue(raw.label, `${path}.label`, errors);
|
|
2256
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
|
|
2257
|
+
const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2258
|
+
return {
|
|
2259
|
+
tierId,
|
|
2260
|
+
...label ? { label } : {},
|
|
2261
|
+
...bodyMarkdown ? { bodyMarkdown } : {},
|
|
2262
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/member-content-validation.ts
|
|
2267
|
+
function validateMemberContentDocument(input, options = {}) {
|
|
2268
|
+
const errors = [];
|
|
2269
|
+
const root = record(input);
|
|
2270
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2271
|
+
unknownKeys(root, ROOT_KEYS, "document", errors);
|
|
2272
|
+
if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2273
|
+
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
2274
|
+
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
2275
|
+
const sourceRaw = record(root.source);
|
|
2276
|
+
let source = { type: "chapter", appId: "invalid" };
|
|
2277
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2278
|
+
else {
|
|
2279
|
+
unknownKeys(sourceRaw, SOURCE_KEYS, "document.source", errors);
|
|
2280
|
+
const type = sourceRaw.type;
|
|
2281
|
+
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
2282
|
+
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
2283
|
+
if (appId && !ID2.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2284
|
+
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2285
|
+
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2286
|
+
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
2287
|
+
}
|
|
2288
|
+
let target;
|
|
2289
|
+
if (root.target !== void 0) {
|
|
2290
|
+
const targetRaw = record(root.target);
|
|
2291
|
+
if (!targetRaw) errors.push("document.target must be an object");
|
|
2292
|
+
else {
|
|
2293
|
+
unknownKeys(targetRaw, TARGET_KEYS, "document.target", errors);
|
|
2294
|
+
const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
|
|
2295
|
+
const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
|
|
2296
|
+
target = { chapterId, environment };
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
|
|
2300
|
+
if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
|
|
2301
|
+
if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
|
|
2302
|
+
const rawPages = Array.isArray(root.pages) ? root.pages : [];
|
|
2303
|
+
if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
|
|
2304
|
+
const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
|
|
2305
|
+
const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
|
|
2306
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2307
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2308
|
+
const pages = [];
|
|
2309
|
+
for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2310
|
+
const path = `document.pages[${index}]`;
|
|
2311
|
+
const raw = record(value);
|
|
2312
|
+
if (!raw) {
|
|
2313
|
+
errors.push(`${path} must be an object`);
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
unknownKeys(raw, PAGE_KEYS, path, errors);
|
|
2317
|
+
const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2318
|
+
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2319
|
+
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2320
|
+
if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2321
|
+
if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2322
|
+
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2323
|
+
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2324
|
+
ids.add(id4);
|
|
2325
|
+
slugs.add(slug);
|
|
2326
|
+
const navRaw = record(raw.navigation);
|
|
2327
|
+
let navigation = { section: "General", order: index };
|
|
2328
|
+
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2329
|
+
else {
|
|
2330
|
+
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2331
|
+
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2332
|
+
const order = navRaw.order;
|
|
2333
|
+
if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
|
|
2334
|
+
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2335
|
+
}
|
|
2336
|
+
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2337
|
+
navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
|
|
2338
|
+
}
|
|
2339
|
+
const audienceRaw = record(raw.audience);
|
|
2340
|
+
let audience = { kind: "all-members" };
|
|
2341
|
+
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2342
|
+
else {
|
|
2343
|
+
unknownKeys(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
|
|
2344
|
+
if (audienceRaw.kind === "all-members") {
|
|
2345
|
+
if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
|
|
2346
|
+
} else if (audienceRaw.kind === "tiers") {
|
|
2347
|
+
if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
|
|
2348
|
+
errors.push(`${path}.audience.tierIds must be a non-empty array`);
|
|
2349
|
+
}
|
|
2350
|
+
const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
|
|
2351
|
+
const normalized = [];
|
|
2352
|
+
for (const [tierIndex, value2] of values.entries()) {
|
|
2353
|
+
const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
|
|
2354
|
+
if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
|
|
2355
|
+
if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
|
|
2356
|
+
normalized.push(tier);
|
|
2357
|
+
}
|
|
2358
|
+
audience = { kind: "tiers", tierIds: normalized };
|
|
2359
|
+
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2360
|
+
}
|
|
2361
|
+
const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
|
|
2362
|
+
const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
|
|
2363
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
|
|
2364
|
+
let upsell;
|
|
2365
|
+
if (raw.upsell !== void 0) {
|
|
2366
|
+
const upsellRaw = record(raw.upsell);
|
|
2367
|
+
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2368
|
+
else {
|
|
2369
|
+
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
2370
|
+
const owner = upsellRaw.owner;
|
|
2371
|
+
if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
|
|
2372
|
+
if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
|
|
2373
|
+
if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
|
|
2374
|
+
if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
|
|
2375
|
+
const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
|
|
2376
|
+
if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
|
|
2377
|
+
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
pages.push({ id: id4, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2381
|
+
}
|
|
2382
|
+
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2383
|
+
for (const [index, page] of pages.entries()) {
|
|
2384
|
+
const parentId = page.navigation.parentId;
|
|
2385
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
|
|
2386
|
+
if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
|
|
2387
|
+
}
|
|
2388
|
+
for (const page of pages) {
|
|
2389
|
+
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
2390
|
+
let parentId = page.navigation.parentId;
|
|
2391
|
+
while (parentId) {
|
|
2392
|
+
if (seen.has(parentId)) {
|
|
2393
|
+
errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
seen.add(parentId);
|
|
2397
|
+
parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
let upsellOverrides;
|
|
2401
|
+
if (root.upsellOverrides !== void 0) {
|
|
2402
|
+
if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
|
|
2403
|
+
if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
|
|
2404
|
+
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2405
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2406
|
+
upsellOverrides = [];
|
|
2407
|
+
for (const [index, value] of values.entries()) {
|
|
2408
|
+
const path = `document.upsellOverrides[${index}]`;
|
|
2409
|
+
const raw = record(value);
|
|
2410
|
+
if (!raw) {
|
|
2411
|
+
errors.push(`${path} must be an object`);
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
unknownKeys(raw, OVERRIDE_KEYS, path, errors);
|
|
2415
|
+
const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2416
|
+
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2417
|
+
seen.add(pageId);
|
|
2418
|
+
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2419
|
+
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2420
|
+
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2421
|
+
const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2422
|
+
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
if (errors.length) return { ok: false, errors };
|
|
2426
|
+
return { ok: true, value: {
|
|
2427
|
+
schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
|
|
2428
|
+
revision: Number(root.revision),
|
|
2429
|
+
generatedAt: Number(root.generatedAt),
|
|
2430
|
+
source,
|
|
2431
|
+
...target ? { target } : {},
|
|
2432
|
+
pages,
|
|
2433
|
+
...upsellOverrides ? { upsellOverrides } : {}
|
|
2434
|
+
} };
|
|
2435
|
+
}
|
|
2436
|
+
function memberContentDocument(input, options = {}) {
|
|
2437
|
+
const result = validateMemberContentDocument(input, options);
|
|
2438
|
+
if (!result.ok) throw new TypeError(`Invalid member content document: ${result.errors.join("; ")}`);
|
|
2439
|
+
return result.value;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// src/member-content.ts
|
|
2443
|
+
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
2444
|
+
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
2445
|
+
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
2446
|
+
var pageOrder = (a, b) => a.navigation.section.localeCompare(b.navigation.section) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
|
|
2447
|
+
function composeMemberContent(parent, chapter) {
|
|
2448
|
+
const pages = [];
|
|
2449
|
+
const parentPages = /* @__PURE__ */ new Map();
|
|
2450
|
+
for (const page of parent?.pages ?? []) {
|
|
2451
|
+
const effective = { ...page, source: parent.source };
|
|
2452
|
+
parentPages.set(page.id, effective);
|
|
2453
|
+
pages.push(effective);
|
|
2454
|
+
}
|
|
2455
|
+
for (const override of chapter?.upsellOverrides ?? []) {
|
|
2456
|
+
const page = parentPages.get(override.pageId);
|
|
2457
|
+
if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
|
|
2458
|
+
}
|
|
2459
|
+
for (const page of chapter?.pages ?? []) {
|
|
2460
|
+
if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
|
|
2461
|
+
throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
|
|
2462
|
+
}
|
|
2463
|
+
pages.push({ ...page, source: chapter.source });
|
|
2464
|
+
}
|
|
2465
|
+
return {
|
|
2466
|
+
pages: pages.sort(pageOrder),
|
|
2467
|
+
parentRevision: parent?.revision ?? null,
|
|
2468
|
+
chapterRevision: chapter?.revision ?? null
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
2472
|
+
function projectMemberContent(content, viewer, tiers2) {
|
|
2473
|
+
const authority = new Map(tiers2.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
2474
|
+
return {
|
|
2475
|
+
viewer: { ...viewer },
|
|
2476
|
+
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
2477
|
+
pages: content.pages.map((page) => {
|
|
2478
|
+
const canRead = allowed(page, viewer);
|
|
2479
|
+
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
2480
|
+
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
2481
|
+
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
2482
|
+
return {
|
|
2483
|
+
id: page.id,
|
|
2484
|
+
slug: page.slug,
|
|
2485
|
+
title: page.title,
|
|
2486
|
+
...page.summary ? { summary: page.summary } : {},
|
|
2487
|
+
navigation: page.navigation,
|
|
2488
|
+
audience: page.audience,
|
|
2489
|
+
source: page.source,
|
|
2490
|
+
access: canRead ? "allowed" : "locked",
|
|
2491
|
+
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
2492
|
+
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
2493
|
+
...upsell ? { upsell } : {}
|
|
2494
|
+
};
|
|
2495
|
+
})
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// src/member-content-canonical.ts
|
|
2500
|
+
var encoder2 = new TextEncoder();
|
|
2501
|
+
function canonicalValue2(value) {
|
|
2502
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2503
|
+
return JSON.stringify(value);
|
|
2504
|
+
}
|
|
2505
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2506
|
+
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2507
|
+
const record3 = value;
|
|
2508
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2509
|
+
const item = record3[key];
|
|
2510
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2511
|
+
});
|
|
2512
|
+
return `{${entries.join(",")}}`;
|
|
2513
|
+
}
|
|
2514
|
+
function canonicalMemberContent(value) {
|
|
2515
|
+
return canonicalValue2(value);
|
|
2516
|
+
}
|
|
2517
|
+
async function memberContentDigest(document) {
|
|
2518
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
|
|
2519
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2520
|
+
}
|
|
2521
|
+
async function createMemberContentEnvelope(document) {
|
|
2522
|
+
return { document, digest: await memberContentDigest(document) };
|
|
2523
|
+
}
|
|
2524
|
+
|
|
1692
2525
|
// src/email.ts
|
|
1693
2526
|
function render(template, vars) {
|
|
1694
2527
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -1719,10 +2552,10 @@ function planDelivery(input) {
|
|
|
1719
2552
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
1720
2553
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
1721
2554
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
1722
|
-
const
|
|
2555
|
+
const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
1723
2556
|
|
|
1724
2557
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
1725
|
-
return { deliver: true, transport, to, subject, text:
|
|
2558
|
+
return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
|
|
1726
2559
|
}
|
|
1727
2560
|
|
|
1728
2561
|
// src/notify.ts
|
|
@@ -1757,9 +2590,9 @@ async function sendTemplated(deps, input) {
|
|
|
1757
2590
|
error = e instanceof Error ? e.message : String(e);
|
|
1758
2591
|
}
|
|
1759
2592
|
}
|
|
1760
|
-
const
|
|
2593
|
+
const id4 = deps.newId();
|
|
1761
2594
|
const row = {
|
|
1762
|
-
id:
|
|
2595
|
+
id: id4,
|
|
1763
2596
|
groupId: input.group.id,
|
|
1764
2597
|
to: decision.to,
|
|
1765
2598
|
template: input.template,
|
|
@@ -1773,7 +2606,7 @@ async function sendTemplated(deps, input) {
|
|
|
1773
2606
|
...messageId ? { messageId } : {},
|
|
1774
2607
|
...error ? { error } : {}
|
|
1775
2608
|
};
|
|
1776
|
-
await deps.db.transact([{ t: "update", ns: "emailLog", id:
|
|
2609
|
+
await deps.db.transact([{ t: "update", ns: "emailLog", id: id4, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
|
|
1777
2610
|
return error ? { sent: false, reason: error } : { sent: true };
|
|
1778
2611
|
}
|
|
1779
2612
|
function emailGroupFrom(row) {
|
|
@@ -1809,8 +2642,26 @@ function findApplicationRef(obj) {
|
|
|
1809
2642
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
1810
2643
|
return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
|
|
1811
2644
|
}
|
|
2645
|
+
function findGiftRef(obj) {
|
|
2646
|
+
const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
|
|
2647
|
+
const pick = (metadata) => typeof metadata.giftIntentId === "string" ? metadata.giftIntentId : void 0;
|
|
2648
|
+
const parent = obj.parent;
|
|
2649
|
+
const subscription = obj.subscription_details ?? parent?.subscription_details;
|
|
2650
|
+
const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
|
|
2651
|
+
const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
|
|
2652
|
+
return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
|
|
2653
|
+
}
|
|
1812
2654
|
function normalizeWebhookEvent(event) {
|
|
1813
2655
|
const obj = event.data?.object ?? {};
|
|
2656
|
+
const gift = findGiftRef(obj);
|
|
2657
|
+
if (gift.giftIntentId) {
|
|
2658
|
+
if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
|
|
2659
|
+
return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
|
|
2660
|
+
}
|
|
2661
|
+
if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
|
|
2662
|
+
if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
|
|
2663
|
+
return { kind: "ignored", type: event.type };
|
|
2664
|
+
}
|
|
1814
2665
|
const ref = findApplicationRef(obj);
|
|
1815
2666
|
switch (event.type) {
|
|
1816
2667
|
case "invoice.paid": {
|
|
@@ -1844,6 +2695,112 @@ function canceledPatch() {
|
|
|
1844
2695
|
return { canceled: true };
|
|
1845
2696
|
}
|
|
1846
2697
|
|
|
2698
|
+
// src/join-continuation.ts
|
|
2699
|
+
function resolveJoinContinuation(input) {
|
|
2700
|
+
if (input.status === "approved") return "accepted";
|
|
2701
|
+
if (input.interviewWaived) {
|
|
2702
|
+
if (!input.payable) return "approve";
|
|
2703
|
+
if (input.status === input.initialStatus) {
|
|
2704
|
+
return input.subscriptionStarted ? "paymentPending" : "payment";
|
|
2705
|
+
}
|
|
2706
|
+
return "approve";
|
|
2707
|
+
}
|
|
2708
|
+
if (!input.payable) return "booking";
|
|
2709
|
+
if (input.status === input.initialStatus) {
|
|
2710
|
+
return input.subscriptionStarted ? "paymentPending" : "payment";
|
|
2711
|
+
}
|
|
2712
|
+
if (canBook(input.status, input.pipeline)) return "booking";
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// src/gift-offers.ts
|
|
2717
|
+
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2718
|
+
var MAX_GIFT_OFFERS = 20;
|
|
2719
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2720
|
+
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2721
|
+
var CURRENCY = /^[a-z]{3}$/;
|
|
2722
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2723
|
+
var text = (value, path, errors, max) => {
|
|
2724
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2725
|
+
errors.push(`${path} is required`);
|
|
2726
|
+
return "";
|
|
2727
|
+
}
|
|
2728
|
+
if (value.trim().length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2729
|
+
return value.trim();
|
|
2730
|
+
};
|
|
2731
|
+
function validateGiftOfferCatalog(input, tierIds) {
|
|
2732
|
+
const errors = [];
|
|
2733
|
+
const root = record2(input);
|
|
2734
|
+
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2735
|
+
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2736
|
+
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
2737
|
+
if (!Array.isArray(root.offers)) errors.push("document.giftOffers.offers must be an array");
|
|
2738
|
+
const rawOffers = Array.isArray(root.offers) ? root.offers : [];
|
|
2739
|
+
if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
|
|
2740
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2741
|
+
const offers = [];
|
|
2742
|
+
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2743
|
+
const path = `document.giftOffers.offers[${index}]`;
|
|
2744
|
+
const item = record2(raw);
|
|
2745
|
+
if (!item) {
|
|
2746
|
+
errors.push(`${path} must be an object`);
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2750
|
+
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2751
|
+
const id4 = text(item.id, `${path}.id`, errors, 80);
|
|
2752
|
+
if (id4 && !ID3.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2753
|
+
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2754
|
+
ids.add(id4);
|
|
2755
|
+
const targetRaw = record2(item.target);
|
|
2756
|
+
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2757
|
+
if (targetRaw?.type === "tier") {
|
|
2758
|
+
const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
2759
|
+
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
2760
|
+
target = { type: "tier", tierId };
|
|
2761
|
+
} else if (targetRaw?.type === "entitlement") {
|
|
2762
|
+
const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
2763
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2764
|
+
target = { type: "entitlement", entitlementKey };
|
|
2765
|
+
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2766
|
+
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
2767
|
+
const stripePriceId = text(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
2768
|
+
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2769
|
+
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`);
|
|
2770
|
+
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
2771
|
+
if (!CURRENCY.test(currency)) errors.push(`${path}.currency must be a lowercase ISO currency code`);
|
|
2772
|
+
const lifetime = Number(item.redemptionLifetimeHours);
|
|
2773
|
+
if (!Number.isSafeInteger(lifetime) || lifetime < 1 || lifetime > 8760) errors.push(`${path}.redemptionLifetimeHours must be an integer from 1 to 8760`);
|
|
2774
|
+
const activeFrom = item.activeFrom === void 0 ? void 0 : Number(item.activeFrom);
|
|
2775
|
+
const activeUntil = item.activeUntil === void 0 ? void 0 : Number(item.activeUntil);
|
|
2776
|
+
if (activeFrom !== void 0 && (!Number.isSafeInteger(activeFrom) || activeFrom < 1)) errors.push(`${path}.activeFrom must be epoch milliseconds`);
|
|
2777
|
+
if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
|
|
2778
|
+
if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
|
|
2779
|
+
offers.push({
|
|
2780
|
+
id: id4,
|
|
2781
|
+
target,
|
|
2782
|
+
billingMode: item.billingMode,
|
|
2783
|
+
stripePriceId,
|
|
2784
|
+
priceCents: Number(item.priceCents),
|
|
2785
|
+
currency,
|
|
2786
|
+
redemptionLifetimeHours: lifetime,
|
|
2787
|
+
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
2788
|
+
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
2789
|
+
title: text(item.title, `${path}.title`, errors, 160),
|
|
2790
|
+
description: text(item.description, `${path}.description`, errors, 2e3),
|
|
2791
|
+
ctaLabel: text(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
2795
|
+
}
|
|
2796
|
+
var giftOfferActive = (offer, now) => (!offer.activeFrom || offer.activeFrom <= now) && (!offer.activeUntil || offer.activeUntil > now);
|
|
2797
|
+
function publicGiftOffers(catalog, now) {
|
|
2798
|
+
return (catalog?.offers ?? []).filter((offer) => giftOfferActive(offer, now)).map(({ stripePriceId: _stripe, target: _target, ...offer }) => offer);
|
|
2799
|
+
}
|
|
2800
|
+
function findGiftOffer(catalog, offerId, now) {
|
|
2801
|
+
return catalog?.offers.find((offer) => offer.id === offerId && giftOfferActive(offer, now)) ?? null;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
1847
2804
|
// src/network.ts
|
|
1848
2805
|
var import_crm5 = require("@odla-ai/crm");
|
|
1849
2806
|
|
|
@@ -1883,34 +2840,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
1883
2840
|
if (readable && raw.length <= 64) return raw;
|
|
1884
2841
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
1885
2842
|
}
|
|
1886
|
-
function normalizeSharedRecord(
|
|
1887
|
-
if ("input" in
|
|
2843
|
+
function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
2844
|
+
if ("input" in record3 && record3.version === 2) {
|
|
1888
2845
|
return {
|
|
1889
2846
|
version: 2,
|
|
1890
|
-
sourceId:
|
|
1891
|
-
sourceRecordId:
|
|
1892
|
-
type:
|
|
1893
|
-
input:
|
|
2847
|
+
sourceId: record3.source.siteId,
|
|
2848
|
+
sourceRecordId: record3.source.recordId,
|
|
2849
|
+
type: record3.type,
|
|
2850
|
+
input: record3.input
|
|
1894
2851
|
};
|
|
1895
2852
|
}
|
|
1896
|
-
if ("input" in
|
|
2853
|
+
if ("input" in record3) {
|
|
1897
2854
|
return {
|
|
1898
2855
|
version: 1,
|
|
1899
2856
|
sourceId: fallbackSourceId,
|
|
1900
|
-
sourceRecordId:
|
|
1901
|
-
type:
|
|
1902
|
-
input:
|
|
2857
|
+
sourceRecordId: record3.hubRecordId,
|
|
2858
|
+
type: record3.type,
|
|
2859
|
+
input: record3.input
|
|
1903
2860
|
};
|
|
1904
2861
|
}
|
|
1905
|
-
if ("type" in
|
|
1906
|
-
const input = { name:
|
|
2862
|
+
if ("type" in record3 && record3.type === "company") {
|
|
2863
|
+
const input = { name: record3.name };
|
|
1907
2864
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
1908
|
-
if (
|
|
2865
|
+
if (record3[key]) input[key] = record3[key];
|
|
1909
2866
|
}
|
|
1910
2867
|
return {
|
|
1911
2868
|
version: 1,
|
|
1912
2869
|
sourceId: fallbackSourceId,
|
|
1913
|
-
sourceRecordId:
|
|
2870
|
+
sourceRecordId: record3.hubRecordId,
|
|
1914
2871
|
type: "company",
|
|
1915
2872
|
input
|
|
1916
2873
|
};
|
|
@@ -1918,28 +2875,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
|
|
|
1918
2875
|
return {
|
|
1919
2876
|
version: 1,
|
|
1920
2877
|
sourceId: fallbackSourceId,
|
|
1921
|
-
sourceRecordId:
|
|
2878
|
+
sourceRecordId: record3.hubRecordId,
|
|
1922
2879
|
type: "person",
|
|
1923
|
-
input: sharedPersonInput(
|
|
2880
|
+
input: sharedPersonInput(record3)
|
|
1924
2881
|
};
|
|
1925
2882
|
}
|
|
1926
|
-
function sharedRecordFromCrm(crm,
|
|
1927
|
-
if (target.fields && !target.fields[
|
|
1928
|
-
throw new Error(`${target.name} does not accept "${
|
|
2883
|
+
function sharedRecordFromCrm(crm, record3, target, sourceId) {
|
|
2884
|
+
if (target.fields && !target.fields[record3.type]) {
|
|
2885
|
+
throw new Error(`${target.name} does not accept "${record3.type}" records`);
|
|
1929
2886
|
}
|
|
1930
|
-
const def = crm.type(
|
|
1931
|
-
const fields = target.fields?.[
|
|
2887
|
+
const def = crm.type(record3.type);
|
|
2888
|
+
const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
|
|
1932
2889
|
if (!fields) {
|
|
1933
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
2890
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
|
|
1934
2891
|
}
|
|
1935
2892
|
const nameField = def.nameField ?? "name";
|
|
1936
2893
|
const input = {};
|
|
1937
2894
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
1938
|
-
const value =
|
|
2895
|
+
const value = record3.fields?.[field];
|
|
1939
2896
|
if (value !== void 0) input[field] = value;
|
|
1940
2897
|
}
|
|
1941
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
1942
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
2898
|
+
if (input[nameField] === void 0) input[nameField] = record3.name;
|
|
2899
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
|
|
1943
2900
|
}
|
|
1944
2901
|
async function upsertPerson(deps, opts) {
|
|
1945
2902
|
const email = opts.email.toLowerCase();
|
|
@@ -1953,11 +2910,11 @@ async function upsertPerson(deps, opts) {
|
|
|
1953
2910
|
const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
1954
2911
|
return { recordId: created.id };
|
|
1955
2912
|
}
|
|
1956
|
-
async function findSharedRecord(deps,
|
|
2913
|
+
async function findSharedRecord(deps, record3, tag) {
|
|
1957
2914
|
const structured = await (0, import_crm5.getRecordByOrigin)(
|
|
1958
2915
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
1959
|
-
|
|
1960
|
-
|
|
2916
|
+
record3.sourceId,
|
|
2917
|
+
record3.sourceRecordId
|
|
1961
2918
|
);
|
|
1962
2919
|
if (structured) return structured;
|
|
1963
2920
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -1966,53 +2923,53 @@ async function findSharedRecord(deps, record, tag) {
|
|
|
1966
2923
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
1967
2924
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1968
2925
|
}
|
|
1969
|
-
const def = deps.crm.type(
|
|
2926
|
+
const def = deps.crm.type(record3.type);
|
|
1970
2927
|
const emailField = def.emailField;
|
|
1971
|
-
if (emailField && typeof
|
|
1972
|
-
const primaryEmail =
|
|
2928
|
+
if (emailField && typeof record3.input[emailField] === "string") {
|
|
2929
|
+
const primaryEmail = record3.input[emailField].toLowerCase();
|
|
1973
2930
|
const found = await deps.db.query({
|
|
1974
|
-
crm_record: { $: { where: { type:
|
|
2931
|
+
crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
|
|
1975
2932
|
});
|
|
1976
2933
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1977
2934
|
}
|
|
1978
|
-
const domain =
|
|
2935
|
+
const domain = record3.input.domain;
|
|
1979
2936
|
const domainSlot = def.fields.domain?.slot;
|
|
1980
2937
|
if (typeof domain === "string" && domainSlot) {
|
|
1981
2938
|
const found = await deps.db.query({
|
|
1982
|
-
crm_record: { $: { where: { type:
|
|
2939
|
+
crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
|
|
1983
2940
|
});
|
|
1984
2941
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1985
2942
|
}
|
|
1986
2943
|
const nameField = def.nameField ?? "name";
|
|
1987
|
-
const name =
|
|
1988
|
-
if (
|
|
2944
|
+
const name = record3.input[nameField];
|
|
2945
|
+
if (record3.type === "company" && typeof name === "string" && name.trim()) {
|
|
1989
2946
|
const found = await deps.db.query({
|
|
1990
|
-
crm_record: { $: { where: { type:
|
|
2947
|
+
crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
|
|
1991
2948
|
});
|
|
1992
2949
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
1993
2950
|
}
|
|
1994
2951
|
return void 0;
|
|
1995
2952
|
}
|
|
1996
2953
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
1997
|
-
const
|
|
1998
|
-
if (!
|
|
2954
|
+
const record3 = normalizeSharedRecord(shared, options.sourceId);
|
|
2955
|
+
if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
|
|
1999
2956
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
2000
2957
|
}
|
|
2001
|
-
if (options.sourceId && options.sourceId !==
|
|
2958
|
+
if (options.sourceId && options.sourceId !== record3.sourceId) {
|
|
2002
2959
|
throw new Error("signed sender does not match payload source");
|
|
2003
2960
|
}
|
|
2004
|
-
const tag = networkSourceTag(
|
|
2961
|
+
const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
|
|
2005
2962
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
2006
|
-
const existing = await findSharedRecord(deps,
|
|
2963
|
+
const existing = await findSharedRecord(deps, record3, tag);
|
|
2007
2964
|
let recordId;
|
|
2008
2965
|
if (existing && typeof existing.id === "string") {
|
|
2009
|
-
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input:
|
|
2966
|
+
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record3.input });
|
|
2010
2967
|
recordId = existing.id;
|
|
2011
2968
|
} else {
|
|
2012
|
-
recordId = `network_${shortHash(`${
|
|
2969
|
+
recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
|
|
2013
2970
|
await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
2014
|
-
type:
|
|
2015
|
-
input:
|
|
2971
|
+
type: record3.type,
|
|
2972
|
+
input: record3.input,
|
|
2016
2973
|
mutationId: `share-create:${tag}`
|
|
2017
2974
|
});
|
|
2018
2975
|
}
|
|
@@ -2022,10 +2979,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
2022
2979
|
);
|
|
2023
2980
|
await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
|
|
2024
2981
|
recordId,
|
|
2025
|
-
sourceId:
|
|
2026
|
-
sourceRecordId:
|
|
2982
|
+
sourceId: record3.sourceId,
|
|
2983
|
+
sourceRecordId: record3.sourceRecordId,
|
|
2027
2984
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
2028
|
-
payloadVersion:
|
|
2985
|
+
payloadVersion: record3.version
|
|
2029
2986
|
});
|
|
2030
2987
|
return { recordId };
|
|
2031
2988
|
}
|
|
@@ -2241,9 +3198,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
|
|
|
2241
3198
|
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
2242
3199
|
if (!found.ok) return false;
|
|
2243
3200
|
const users = await found.json().catch(() => null);
|
|
2244
|
-
const
|
|
2245
|
-
if (!
|
|
2246
|
-
return updateClerkUserMetadata(secretKey,
|
|
3201
|
+
const id4 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
3202
|
+
if (!id4) return false;
|
|
3203
|
+
return updateClerkUserMetadata(secretKey, id4, input, fetchImpl);
|
|
2247
3204
|
}
|
|
2248
3205
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
2249
3206
|
const { path, body } = clerkUserRequest(input);
|
|
@@ -2299,8 +3256,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
|
|
|
2299
3256
|
const user = Array.isArray(data) ? data[0] : void 0;
|
|
2300
3257
|
return user ? toRecord(user) : null;
|
|
2301
3258
|
}
|
|
2302
|
-
async function clerkGetUser(secretKey,
|
|
2303
|
-
const data = await clerkGet(`/v1/users/${encodeURIComponent(
|
|
3259
|
+
async function clerkGetUser(secretKey, id4, fetchImpl = fetch) {
|
|
3260
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id4)}`, secretKey, fetchImpl).catch(() => null);
|
|
2304
3261
|
return data ? toRecord(data) : null;
|
|
2305
3262
|
}
|
|
2306
3263
|
async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
@@ -2309,15 +3266,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
2309
3266
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
2310
3267
|
const page = Array.isArray(data) ? data : [];
|
|
2311
3268
|
for (const u of page) {
|
|
2312
|
-
const
|
|
2313
|
-
if (
|
|
3269
|
+
const record3 = toRecord(u);
|
|
3270
|
+
if (record3) out.push(record3);
|
|
2314
3271
|
}
|
|
2315
3272
|
if (page.length < PAGE) break;
|
|
2316
3273
|
}
|
|
2317
3274
|
return out;
|
|
2318
3275
|
}
|
|
2319
|
-
async function clerkSetRole(secretKey,
|
|
2320
|
-
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(
|
|
3276
|
+
async function clerkSetRole(secretKey, id4, role, fetchImpl = fetch) {
|
|
3277
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id4)}/metadata`, {
|
|
2321
3278
|
method: "PATCH",
|
|
2322
3279
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
2323
3280
|
body: JSON.stringify({ public_metadata: { role } })
|
|
@@ -2412,7 +3369,8 @@ function applicationSummary(app) {
|
|
|
2412
3369
|
meetingLink: app.meetingLink ?? null,
|
|
2413
3370
|
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
2414
3371
|
renewalAt: app.renewalAt ?? null,
|
|
2415
|
-
canceled: app.canceled === true
|
|
3372
|
+
canceled: app.canceled === true,
|
|
3373
|
+
tierId: app.tierId ?? null
|
|
2416
3374
|
};
|
|
2417
3375
|
}
|
|
2418
3376
|
function memberApplication(app, meeting, defaultTimezone) {
|
|
@@ -2693,15 +3651,15 @@ function rowToTier(row, groupId) {
|
|
|
2693
3651
|
}
|
|
2694
3652
|
async function loadTiers(db, group) {
|
|
2695
3653
|
const groupId = String(group.id ?? "");
|
|
2696
|
-
let
|
|
3654
|
+
let rows2;
|
|
2697
3655
|
try {
|
|
2698
|
-
|
|
3656
|
+
rows2 = (await db.query({
|
|
2699
3657
|
tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
|
|
2700
3658
|
})).tiers;
|
|
2701
3659
|
} catch {
|
|
2702
|
-
|
|
3660
|
+
rows2 = void 0;
|
|
2703
3661
|
}
|
|
2704
|
-
const list = Array.isArray(
|
|
3662
|
+
const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
|
|
2705
3663
|
return resolveTiers(group, list);
|
|
2706
3664
|
}
|
|
2707
3665
|
async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
@@ -2712,37 +3670,11 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
|
|
|
2712
3670
|
var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
|
|
2713
3671
|
var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
2714
3672
|
|
|
2715
|
-
// src/signup-control-canonical.ts
|
|
2716
|
-
var encoder = new TextEncoder();
|
|
2717
|
-
function canonicalValue(value) {
|
|
2718
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2719
|
-
return JSON.stringify(value);
|
|
2720
|
-
}
|
|
2721
|
-
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2722
|
-
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2723
|
-
const record = value;
|
|
2724
|
-
const entries = Object.keys(record).sort().flatMap((key) => {
|
|
2725
|
-
const item = record[key];
|
|
2726
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2727
|
-
});
|
|
2728
|
-
return `{${entries.join(",")}}`;
|
|
2729
|
-
}
|
|
2730
|
-
function canonicalSignupControl(value) {
|
|
2731
|
-
return canonicalValue(value);
|
|
2732
|
-
}
|
|
2733
|
-
async function signupControlDigest(document) {
|
|
2734
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
2735
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2736
|
-
}
|
|
2737
|
-
async function createSignupControlEnvelope(document) {
|
|
2738
|
-
return { document, digest: await signupControlDigest(document) };
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
3673
|
// src/membership-page.ts
|
|
2742
3674
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
2743
3675
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2744
3676
|
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
2745
|
-
var
|
|
3677
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
2746
3678
|
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
2747
3679
|
"tierId",
|
|
2748
3680
|
"variant",
|
|
@@ -2761,12 +3693,12 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
|
2761
3693
|
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
2762
3694
|
var MAX_TIERS = 20;
|
|
2763
3695
|
var MAX_BENEFITS = 24;
|
|
2764
|
-
var
|
|
3696
|
+
var MAX_MARKDOWN2 = 65536;
|
|
2765
3697
|
var MAX_LABEL = 160;
|
|
2766
3698
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2767
|
-
function rejectUnknownKeys(value,
|
|
3699
|
+
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
2768
3700
|
for (const key of Object.keys(value)) {
|
|
2769
|
-
if (!
|
|
3701
|
+
if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2770
3702
|
}
|
|
2771
3703
|
}
|
|
2772
3704
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -2778,16 +3710,16 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
2778
3710
|
errors.push(`${path} must be a string`);
|
|
2779
3711
|
return void 0;
|
|
2780
3712
|
}
|
|
2781
|
-
const
|
|
2782
|
-
if (required && !
|
|
2783
|
-
if (
|
|
2784
|
-
return
|
|
3713
|
+
const text3 = value.trim();
|
|
3714
|
+
if (required && !text3) errors.push(`${path} is required`);
|
|
3715
|
+
if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
3716
|
+
return text3;
|
|
2785
3717
|
}
|
|
2786
3718
|
function validateMembershipPageDocument(input, options = {}) {
|
|
2787
3719
|
const errors = [];
|
|
2788
3720
|
const root = objectRecord(input);
|
|
2789
3721
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2790
|
-
rejectUnknownKeys(root,
|
|
3722
|
+
rejectUnknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
2791
3723
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
2792
3724
|
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
2793
3725
|
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
@@ -2830,8 +3762,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2830
3762
|
else {
|
|
2831
3763
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
2832
3764
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
2833
|
-
const
|
|
2834
|
-
return
|
|
3765
|
+
const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
3766
|
+
return text3 ? [text3] : [];
|
|
2835
3767
|
});
|
|
2836
3768
|
}
|
|
2837
3769
|
}
|
|
@@ -2841,7 +3773,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2841
3773
|
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
2842
3774
|
else {
|
|
2843
3775
|
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
2844
|
-
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors,
|
|
3776
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN2, true);
|
|
2845
3777
|
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
2846
3778
|
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
2847
3779
|
}
|
|
@@ -2863,10 +3795,10 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2863
3795
|
}
|
|
2864
3796
|
}
|
|
2865
3797
|
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
2866
|
-
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors,
|
|
3798
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN2);
|
|
2867
3799
|
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
2868
3800
|
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
2869
|
-
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors,
|
|
3801
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN2);
|
|
2870
3802
|
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
2871
3803
|
tiers2.push({
|
|
2872
3804
|
tierId,
|
|
@@ -2888,7 +3820,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
2888
3820
|
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
2889
3821
|
}
|
|
2890
3822
|
}
|
|
2891
|
-
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors,
|
|
3823
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN2);
|
|
2892
3824
|
if (errors.length) return { ok: false, errors };
|
|
2893
3825
|
return {
|
|
2894
3826
|
ok: true,
|
|
@@ -2907,11 +3839,11 @@ function membershipPageDocument(input, options = {}) {
|
|
|
2907
3839
|
}
|
|
2908
3840
|
|
|
2909
3841
|
// src/signup-control-validate.ts
|
|
2910
|
-
var
|
|
3842
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
2911
3843
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
2912
3844
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2913
|
-
var
|
|
2914
|
-
var
|
|
3845
|
+
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
3846
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set([
|
|
2915
3847
|
"schemaVersion",
|
|
2916
3848
|
"revision",
|
|
2917
3849
|
"generatedAt",
|
|
@@ -2922,14 +3854,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
2922
3854
|
"application",
|
|
2923
3855
|
"policy",
|
|
2924
3856
|
"scheduling",
|
|
2925
|
-
"theme"
|
|
3857
|
+
"theme",
|
|
3858
|
+
"giftOffers"
|
|
2926
3859
|
]);
|
|
2927
3860
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
2928
3861
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2929
|
-
function
|
|
2930
|
-
for (const key of Object.keys(value)) if (!
|
|
3862
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
3863
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2931
3864
|
}
|
|
2932
|
-
function
|
|
3865
|
+
function text2(value, path, errors, max = 65536) {
|
|
2933
3866
|
if (typeof value !== "string" || !value.trim()) {
|
|
2934
3867
|
errors.push(`${path} is required`);
|
|
2935
3868
|
return "";
|
|
@@ -2951,26 +3884,26 @@ function tiersOf(value, errors) {
|
|
|
2951
3884
|
errors.push(`${path} must be an object`);
|
|
2952
3885
|
return [];
|
|
2953
3886
|
}
|
|
2954
|
-
|
|
2955
|
-
const
|
|
2956
|
-
if (
|
|
2957
|
-
if (seen.has(
|
|
2958
|
-
seen.add(
|
|
2959
|
-
const name =
|
|
3887
|
+
unknownKeys2(item, TIER_KEYS2, path, errors);
|
|
3888
|
+
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
3889
|
+
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3890
|
+
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
3891
|
+
seen.add(id4);
|
|
3892
|
+
const name = text2(item.name, `${path}.name`, errors, 160);
|
|
2960
3893
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
2961
3894
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
2962
3895
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
2963
3896
|
errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
|
|
2964
3897
|
}
|
|
2965
|
-
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !
|
|
3898
|
+
if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
|
|
2966
3899
|
errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2967
3900
|
}
|
|
2968
3901
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
2969
3902
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
2970
3903
|
}
|
|
2971
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
3904
|
+
const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
2972
3905
|
return [{
|
|
2973
|
-
id:
|
|
3906
|
+
id: id4,
|
|
2974
3907
|
name,
|
|
2975
3908
|
available: item.available === true,
|
|
2976
3909
|
sortOrder: Number(item.sortOrder) || 0,
|
|
@@ -2987,7 +3920,7 @@ function applicationOf(value, errors) {
|
|
|
2987
3920
|
errors.push("document.application must be an object");
|
|
2988
3921
|
return void 0;
|
|
2989
3922
|
}
|
|
2990
|
-
|
|
3923
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
2991
3924
|
try {
|
|
2992
3925
|
const resolved = resolveApplication(input);
|
|
2993
3926
|
const copy = recordOf(input.copy);
|
|
@@ -3012,12 +3945,12 @@ function policyOf(value, errors) {
|
|
|
3012
3945
|
errors.push("document.policy must be an object");
|
|
3013
3946
|
return void 0;
|
|
3014
3947
|
}
|
|
3015
|
-
const
|
|
3016
|
-
|
|
3948
|
+
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
3949
|
+
unknownKeys2(input, allowed2, "document.policy", errors);
|
|
3017
3950
|
const result = {};
|
|
3018
|
-
for (const key of
|
|
3951
|
+
for (const key of allowed2) {
|
|
3019
3952
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3020
|
-
result[key] =
|
|
3953
|
+
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
3021
3954
|
}
|
|
3022
3955
|
return result;
|
|
3023
3956
|
}
|
|
@@ -3029,7 +3962,7 @@ function themeOf(value, errors) {
|
|
|
3029
3962
|
errors.push("document.theme.tokens must be an object");
|
|
3030
3963
|
return void 0;
|
|
3031
3964
|
}
|
|
3032
|
-
|
|
3965
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
3033
3966
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
3034
3967
|
let serializedLength = 0;
|
|
3035
3968
|
for (const [key, token] of Object.entries(tokens)) {
|
|
@@ -3049,17 +3982,17 @@ function validateSignupControlDocument(input) {
|
|
|
3049
3982
|
const errors = [];
|
|
3050
3983
|
const root = recordOf(input);
|
|
3051
3984
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3052
|
-
|
|
3985
|
+
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
3053
3986
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3054
3987
|
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
3055
3988
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
3056
3989
|
const source = recordOf(root.source);
|
|
3057
3990
|
const target = recordOf(root.target);
|
|
3058
|
-
const appId =
|
|
3059
|
-
const chapterId =
|
|
3060
|
-
const environment =
|
|
3061
|
-
if (appId && !
|
|
3062
|
-
if (chapterId && !
|
|
3991
|
+
const appId = text2(source?.appId, "document.source.appId", errors, 63);
|
|
3992
|
+
const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
3993
|
+
const environment = text2(target?.environment, "document.target.environment", errors, 63);
|
|
3994
|
+
if (appId && !SLUG6.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3995
|
+
if (chapterId && !SLUG6.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3063
3996
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
3064
3997
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
3065
3998
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
@@ -3078,6 +4011,8 @@ function validateSignupControlDocument(input) {
|
|
|
3078
4011
|
else scheduling = root.scheduling;
|
|
3079
4012
|
}
|
|
3080
4013
|
const theme = themeOf(root.theme, errors);
|
|
4014
|
+
const giftOffers = root.giftOffers === void 0 ? void 0 : validateGiftOfferCatalog(root.giftOffers, new Set(tiers2.map((tier) => tier.id)));
|
|
4015
|
+
if (giftOffers && !giftOffers.ok) errors.push(...giftOffers.errors);
|
|
3081
4016
|
if (errors.length || !page.ok) return { ok: false, errors };
|
|
3082
4017
|
return { ok: true, value: {
|
|
3083
4018
|
schemaVersion: 1,
|
|
@@ -3090,7 +4025,8 @@ function validateSignupControlDocument(input) {
|
|
|
3090
4025
|
...application ? { application } : {},
|
|
3091
4026
|
...policy ? { policy } : {},
|
|
3092
4027
|
...scheduling ? { scheduling } : {},
|
|
3093
|
-
...theme ? { theme } : {}
|
|
4028
|
+
...theme ? { theme } : {},
|
|
4029
|
+
...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
|
|
3094
4030
|
} };
|
|
3095
4031
|
}
|
|
3096
4032
|
|
|
@@ -3206,7 +4142,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
|
3206
4142
|
|
|
3207
4143
|
// src/membership-markdown.ts
|
|
3208
4144
|
var MAX_BENEFITS2 = 24;
|
|
3209
|
-
var
|
|
4145
|
+
var MAX_MARKDOWN3 = 65536;
|
|
3210
4146
|
var MAX_LABEL2 = 160;
|
|
3211
4147
|
var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
|
|
3212
4148
|
var MARKDOWN_HEADING = /^#{1,6}\s+/;
|
|
@@ -3231,17 +4167,17 @@ function markdownBenefits(lines, path, errors) {
|
|
|
3231
4167
|
errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
|
|
3232
4168
|
continue;
|
|
3233
4169
|
}
|
|
3234
|
-
const
|
|
3235
|
-
if (
|
|
3236
|
-
if (
|
|
4170
|
+
const text3 = (match[1] ?? "").trim();
|
|
4171
|
+
if (text3.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
|
|
4172
|
+
if (text3) benefits.push(text3);
|
|
3237
4173
|
}
|
|
3238
4174
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
3239
4175
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
3240
4176
|
}
|
|
3241
4177
|
function applyMembershipTierMarkdown(presentation, source) {
|
|
3242
4178
|
if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
|
|
3243
|
-
if (source.length >
|
|
3244
|
-
return { ok: false, errors: [`membership Markdown must be at most ${
|
|
4179
|
+
if (source.length > MAX_MARKDOWN3) {
|
|
4180
|
+
return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN3.toLocaleString("en-US")} characters`] };
|
|
3245
4181
|
}
|
|
3246
4182
|
if (!source.trim()) return { ok: true, value: { ...presentation } };
|
|
3247
4183
|
const errors = [];
|
|
@@ -3333,7 +4269,7 @@ var exclusions = (chapterId, paidTierIds) => [
|
|
|
3333
4269
|
id: chapterId,
|
|
3334
4270
|
reason: "provider_identity",
|
|
3335
4271
|
paths: [
|
|
3336
|
-
...paidTierIds.map((
|
|
4272
|
+
...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
|
|
3337
4273
|
"groups.stripePublishableKey",
|
|
3338
4274
|
"applications.stripeCustomerId",
|
|
3339
4275
|
"applications.stripeSubscriptionId",
|
|
@@ -3375,9 +4311,9 @@ async function exportChapterPromotion(input) {
|
|
|
3375
4311
|
sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
|
|
3376
4312
|
receiptDigests: [input.digest]
|
|
3377
4313
|
};
|
|
3378
|
-
const artifact = (kind,
|
|
4314
|
+
const artifact = (kind, id4, content) => ({
|
|
3379
4315
|
kind,
|
|
3380
|
-
id:
|
|
4316
|
+
id: id4,
|
|
3381
4317
|
sourceRevision: revision,
|
|
3382
4318
|
provenance: promotionProvenance,
|
|
3383
4319
|
content: (0, import_promotion.canonicalPromotionValue)(content)
|