@odla-ai/chapter 0.36.0 → 0.38.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 +136 -2
- package/dist/{chunk-I45VYBUV.js → chunk-BIYWPPH6.js} +233 -140
- package/dist/chunk-BIYWPPH6.js.map +1 -0
- package/dist/{chunk-LWM32REV.js → chunk-SI3JHSEX.js} +211 -64
- package/dist/chunk-SI3JHSEX.js.map +1 -0
- package/dist/{copy-context-0k0-yWHL.d.ts → copy-context-Pbs3keLx.d.ts} +174 -28
- package/dist/index.cjs +807 -345
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +295 -36
- package/dist/index.d.ts +295 -36
- package/dist/index.js +807 -345
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +27 -3
- package/dist/ui/admin/index.js +5 -1
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +8 -2
- package/dist/ui/member/index.d.ts +63 -4
- package/dist/ui/member/index.js +3 -1
- package/dist/worker/index.cjs +1032 -354
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +151 -28
- package/dist/worker/index.d.ts +151 -28
- package/dist/worker/index.js +1025 -347
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/greenfield.md +27 -0
- package/dist/chunk-I45VYBUV.js.map +0 -1
- package/dist/chunk-LWM32REV.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -22,12 +22,16 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ADMISSION_GRANTS_PATH: () => ADMISSION_GRANTS_PATH,
|
|
24
24
|
ADMISSION_GRANTS_SCHEMA_VERSION: () => ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
25
|
+
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION: () => CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
25
26
|
CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
|
|
26
27
|
DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
|
|
27
28
|
DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
|
|
28
29
|
GIFT_OFFER_CATALOG_VERSION: () => GIFT_OFFER_CATALOG_VERSION,
|
|
29
30
|
MAX_GIFT_OFFERS: () => MAX_GIFT_OFFERS,
|
|
30
31
|
MEMBERSHIP_PAGE_RENDERER: () => MEMBERSHIP_PAGE_RENDERER,
|
|
32
|
+
MEMBER_CONTENT_CONTROL_PATH: () => MEMBER_CONTENT_CONTROL_PATH,
|
|
33
|
+
MEMBER_CONTENT_PATH: () => MEMBER_CONTENT_PATH,
|
|
34
|
+
MEMBER_CONTENT_SCHEMA_VERSION: () => MEMBER_CONTENT_SCHEMA_VERSION,
|
|
31
35
|
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
32
36
|
SIGNUP_CONTROL_PATH: () => SIGNUP_CONTROL_PATH,
|
|
33
37
|
SIGNUP_CONTROL_SCHEMA_VERSION: () => SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
@@ -35,6 +39,7 @@ __export(index_exports, {
|
|
|
35
39
|
SIGNUP_CONTROL_THEME_STYLE_ID: () => SIGNUP_CONTROL_THEME_STYLE_ID,
|
|
36
40
|
applicantProfile: () => applicantProfile,
|
|
37
41
|
applicationBookingUpdate: () => applicationBookingUpdate,
|
|
42
|
+
applicationFlowFields: () => applicationFlowFields,
|
|
38
43
|
applicationSummary: () => applicationSummary,
|
|
39
44
|
applyMembershipTierMarkdown: () => applyMembershipTierMarkdown,
|
|
40
45
|
backfillCrm: () => backfillCrm,
|
|
@@ -48,9 +53,11 @@ __export(index_exports, {
|
|
|
48
53
|
canChangeRole: () => canChangeRole,
|
|
49
54
|
canTransition: () => canTransition,
|
|
50
55
|
canceledPatch: () => canceledPatch,
|
|
56
|
+
canonicalMemberContent: () => canonicalMemberContent,
|
|
51
57
|
canonicalSignupControl: () => canonicalSignupControl,
|
|
52
58
|
chapterBrandFromTokens: () => chapterBrandFromTokens,
|
|
53
59
|
chapterDb: () => chapterDb,
|
|
60
|
+
chapterFlowFieldContract: () => chapterFlowFieldContract,
|
|
54
61
|
clampArray: () => clampArray,
|
|
55
62
|
clerkGetUser: () => clerkGetUser,
|
|
56
63
|
clerkGetUserByEmail: () => clerkGetUserByEmail,
|
|
@@ -59,10 +66,12 @@ __export(index_exports, {
|
|
|
59
66
|
clerkListUsers: () => clerkListUsers,
|
|
60
67
|
clerkSetRole: () => clerkSetRole,
|
|
61
68
|
clerkUserRequest: () => clerkUserRequest,
|
|
69
|
+
composeMemberContent: () => composeMemberContent,
|
|
62
70
|
createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
|
|
63
71
|
createChapterIntegration: () => createChapterIntegration,
|
|
64
72
|
createClerkInvitation: () => createClerkInvitation,
|
|
65
73
|
createClerkUser: () => createClerkUser,
|
|
74
|
+
createMemberContentEnvelope: () => createMemberContentEnvelope,
|
|
66
75
|
createSignupControlEnvelope: () => createSignupControlEnvelope,
|
|
67
76
|
dashboardMetricData: () => dashboardMetricData,
|
|
68
77
|
defaultCrm: () => defaultCrm,
|
|
@@ -77,6 +86,7 @@ __export(index_exports, {
|
|
|
77
86
|
firstPaymentPatch: () => firstPaymentPatch,
|
|
78
87
|
formatChapterCopy: () => formatChapterCopy,
|
|
79
88
|
formationFields: () => formationFields,
|
|
89
|
+
formationFlowFields: () => formationFlowFields,
|
|
80
90
|
getVaultSecret: () => getVaultSecret,
|
|
81
91
|
giftOfferActive: () => giftOfferActive,
|
|
82
92
|
hasDisclaimerAck: () => hasDisclaimerAck,
|
|
@@ -98,11 +108,14 @@ __export(index_exports, {
|
|
|
98
108
|
meetingCreateRow: () => meetingCreateRow,
|
|
99
109
|
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
100
110
|
memberApplication: () => memberApplication,
|
|
111
|
+
memberContentDigest: () => memberContentDigest,
|
|
112
|
+
memberContentDocument: () => memberContentDocument,
|
|
101
113
|
memberSession: () => memberSession,
|
|
102
114
|
membershipPageDocument: () => membershipPageDocument,
|
|
103
115
|
membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
|
|
104
116
|
networkSourceTag: () => networkSourceTag,
|
|
105
117
|
normalizeAdmissionEmail: () => normalizeAdmissionEmail,
|
|
118
|
+
normalizeChapterEmail: () => normalizeChapterEmail,
|
|
106
119
|
normalizeSharedRecord: () => normalizeSharedRecord,
|
|
107
120
|
normalizeWebhookEvent: () => normalizeWebhookEvent,
|
|
108
121
|
offerableTiers: () => offerableTiers,
|
|
@@ -110,6 +123,7 @@ __export(index_exports, {
|
|
|
110
123
|
personInputFromApp: () => personInputFromApp,
|
|
111
124
|
planDelivery: () => planDelivery,
|
|
112
125
|
projectApplicant: () => projectApplicant,
|
|
126
|
+
projectMemberContent: () => projectMemberContent,
|
|
113
127
|
projectSharedRecord: () => projectSharedRecord,
|
|
114
128
|
publicGiftOffers: () => publicGiftOffers,
|
|
115
129
|
reconcileMeetings: () => reconcileMeetings,
|
|
@@ -124,6 +138,7 @@ __export(index_exports, {
|
|
|
124
138
|
resolveChapterCopy: () => resolveChapterCopy,
|
|
125
139
|
resolveJoinContinuation: () => resolveJoinContinuation,
|
|
126
140
|
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
141
|
+
resolveMemberContent: () => resolveMemberContent,
|
|
127
142
|
resolvePipeline: () => resolvePipeline,
|
|
128
143
|
resolveScheduling: () => resolveScheduling,
|
|
129
144
|
resolveSignupControl: () => resolveSignupControl,
|
|
@@ -147,6 +162,7 @@ __export(index_exports, {
|
|
|
147
162
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
148
163
|
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
149
164
|
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
165
|
+
validateMemberContentDocument: () => validateMemberContentDocument,
|
|
150
166
|
validateMembershipPageDocument: () => validateMembershipPageDocument,
|
|
151
167
|
validateScheduling: () => validateScheduling,
|
|
152
168
|
validateSignupControlDocument: () => validateSignupControlDocument,
|
|
@@ -172,212 +188,10 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
172
188
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
173
189
|
}
|
|
174
190
|
|
|
175
|
-
// src/
|
|
176
|
-
|
|
177
|
-
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
178
|
-
|
|
179
|
-
// src/signup-control-canonical.ts
|
|
180
|
-
var encoder = new TextEncoder();
|
|
181
|
-
function canonicalValue(value) {
|
|
182
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
183
|
-
return JSON.stringify(value);
|
|
184
|
-
}
|
|
185
|
-
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
186
|
-
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
187
|
-
const record2 = value;
|
|
188
|
-
const entries = Object.keys(record2).sort().flatMap((key) => {
|
|
189
|
-
const item = record2[key];
|
|
190
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
191
|
-
});
|
|
192
|
-
return `{${entries.join(",")}}`;
|
|
193
|
-
}
|
|
194
|
-
function canonicalSignupControl(value) {
|
|
195
|
-
return canonicalValue(value);
|
|
196
|
-
}
|
|
197
|
-
async function signupControlDigest(document) {
|
|
198
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
199
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
200
|
-
}
|
|
201
|
-
async function createSignupControlEnvelope(document) {
|
|
202
|
-
return { document, digest: await signupControlDigest(document) };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// src/admission-grants.ts
|
|
206
|
-
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
207
|
-
var MAX_GRANTS = 2e3;
|
|
208
|
-
var MAX_NOTE = 2e3;
|
|
209
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
210
|
-
var isAdmissionEmail = (value) => EMAIL_RE.test(value);
|
|
211
|
-
function normalizeAdmissionEmail(email) {
|
|
191
|
+
// src/email-identity.ts
|
|
192
|
+
function normalizeChapterEmail(email) {
|
|
212
193
|
return email.trim().toLowerCase();
|
|
213
194
|
}
|
|
214
|
-
function validateAdmissionGrantDocument(input) {
|
|
215
|
-
const errors = [];
|
|
216
|
-
const root = input && typeof input === "object" ? input : {};
|
|
217
|
-
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
218
|
-
const revision = Number(root.revision);
|
|
219
|
-
const generatedAt = Number(root.generatedAt);
|
|
220
|
-
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
221
|
-
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
222
|
-
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
223
|
-
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
224
|
-
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
225
|
-
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
226
|
-
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
227
|
-
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
228
|
-
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
229
|
-
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
230
|
-
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
231
|
-
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
232
|
-
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
233
|
-
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
234
|
-
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
235
|
-
const grants = [];
|
|
236
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
237
|
-
const seenEmails = /* @__PURE__ */ new Set();
|
|
238
|
-
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
239
|
-
const grant = raw && typeof raw === "object" ? raw : {};
|
|
240
|
-
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
241
|
-
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
242
|
-
const normalized = normalizeAdmissionEmail(email);
|
|
243
|
-
const decision = grant.decision;
|
|
244
|
-
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
245
|
-
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
246
|
-
if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
247
|
-
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
248
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
249
|
-
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
250
|
-
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
251
|
-
errors.push(`document.grants[${index}].decision is invalid`);
|
|
252
|
-
}
|
|
253
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
254
|
-
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
255
|
-
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
256
|
-
}
|
|
257
|
-
seenIds.add(id4);
|
|
258
|
-
seenEmails.add(normalized);
|
|
259
|
-
grants.push({
|
|
260
|
-
id: id4,
|
|
261
|
-
email,
|
|
262
|
-
decision,
|
|
263
|
-
...note ? { note } : {},
|
|
264
|
-
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
if (errors.length) return { ok: false, errors };
|
|
268
|
-
return {
|
|
269
|
-
ok: true,
|
|
270
|
-
value: {
|
|
271
|
-
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
272
|
-
revision,
|
|
273
|
-
generatedAt,
|
|
274
|
-
source: { appId: sourceId, actor: sourceActor },
|
|
275
|
-
target: { chapterId, environment },
|
|
276
|
-
grants
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
async function createAdmissionGrantEnvelope(document) {
|
|
281
|
-
const checked = validateAdmissionGrantDocument(document);
|
|
282
|
-
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
283
|
-
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
284
|
-
}
|
|
285
|
-
var rows = (value) => Array.isArray(value) ? value : [];
|
|
286
|
-
function asGrant(row) {
|
|
287
|
-
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
288
|
-
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
289
|
-
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
290
|
-
return row;
|
|
291
|
-
}
|
|
292
|
-
async function findAdmissionGrant(db, targetId, email, now) {
|
|
293
|
-
const emailNormalized = normalizeAdmissionEmail(email);
|
|
294
|
-
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
295
|
-
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
296
|
-
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
297
|
-
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
298
|
-
)).sort((a, b) => {
|
|
299
|
-
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
300
|
-
return scope || b.createdAt - a.createdAt;
|
|
301
|
-
});
|
|
302
|
-
return active[0] ?? null;
|
|
303
|
-
}
|
|
304
|
-
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
305
|
-
const where = { sourceId, targetId, environment };
|
|
306
|
-
if (db.aggregate) {
|
|
307
|
-
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
308
|
-
}
|
|
309
|
-
const found = await db.query({ admissionGrantRevisions: {} });
|
|
310
|
-
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
311
|
-
}
|
|
312
|
-
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
313
|
-
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
314
|
-
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
315
|
-
where: { sourceId, targetId, environment },
|
|
316
|
-
order: { revision: "desc" },
|
|
317
|
-
limit: 1
|
|
318
|
-
} } });
|
|
319
|
-
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
320
|
-
return row ? row : null;
|
|
321
|
-
}
|
|
322
|
-
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
323
|
-
const { document, digest } = envelope;
|
|
324
|
-
const current = rows((await db.query({ admissionGrants: { $: {
|
|
325
|
-
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
326
|
-
limit: MAX_GRANTS
|
|
327
|
-
} } })).admissionGrants);
|
|
328
|
-
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
329
|
-
const ops = [];
|
|
330
|
-
for (const row of current) {
|
|
331
|
-
const id4 = String(row.id ?? "");
|
|
332
|
-
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
333
|
-
active: false,
|
|
334
|
-
revokedAt: receivedAt,
|
|
335
|
-
revokedBy: document.source.actor,
|
|
336
|
-
updatedAt: receivedAt,
|
|
337
|
-
updatedBy: document.source.actor
|
|
338
|
-
} });
|
|
339
|
-
}
|
|
340
|
-
for (const grant of document.grants) {
|
|
341
|
-
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
342
|
-
const existing = current.find((row) => row.id === id4);
|
|
343
|
-
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
344
|
-
id: id4,
|
|
345
|
-
sourceGrantId: grant.id,
|
|
346
|
-
sourceScope: "parent",
|
|
347
|
-
sourceId: document.source.appId,
|
|
348
|
-
targetId: document.target.chapterId,
|
|
349
|
-
email: grant.email,
|
|
350
|
-
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
351
|
-
decision: grant.decision,
|
|
352
|
-
...grant.note ? { note: grant.note } : {},
|
|
353
|
-
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
354
|
-
active: true,
|
|
355
|
-
sourceRevision: document.revision,
|
|
356
|
-
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
357
|
-
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
358
|
-
updatedAt: receivedAt,
|
|
359
|
-
updatedBy: document.source.actor
|
|
360
|
-
} });
|
|
361
|
-
const retract = [
|
|
362
|
-
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
363
|
-
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
364
|
-
];
|
|
365
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
366
|
-
}
|
|
367
|
-
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
368
|
-
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
369
|
-
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
370
|
-
id: revisionId,
|
|
371
|
-
sourceId: document.source.appId,
|
|
372
|
-
targetId: document.target.chapterId,
|
|
373
|
-
environment: document.target.environment,
|
|
374
|
-
revision: document.revision,
|
|
375
|
-
digest,
|
|
376
|
-
active: true,
|
|
377
|
-
receivedAt
|
|
378
|
-
} });
|
|
379
|
-
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
380
|
-
}
|
|
381
195
|
|
|
382
196
|
// src/member.ts
|
|
383
197
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
@@ -430,9 +244,9 @@ function resolveApplication(a) {
|
|
|
430
244
|
validateEmail: a?.validateEmail ?? true
|
|
431
245
|
};
|
|
432
246
|
}
|
|
433
|
-
var
|
|
247
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
434
248
|
function isValidEmail(value) {
|
|
435
|
-
return typeof value === "string" &&
|
|
249
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
436
250
|
}
|
|
437
251
|
function clampArray(value, max) {
|
|
438
252
|
if (!Array.isArray(value)) return value;
|
|
@@ -444,14 +258,14 @@ function hasDisclaimerAck(fields) {
|
|
|
444
258
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
445
259
|
function applicantProfile(chapter, fields) {
|
|
446
260
|
const app = chapter.application;
|
|
447
|
-
const
|
|
261
|
+
const allowed2 = (f) => app.profileFields.includes(f);
|
|
448
262
|
const profile = {};
|
|
449
263
|
for (const f of [...app.required, ...app.optional]) {
|
|
450
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
264
|
+
if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
|
|
451
265
|
const v = fields[f];
|
|
452
266
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
453
267
|
}
|
|
454
|
-
if (fields.focus !== void 0 &&
|
|
268
|
+
if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
455
269
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
456
270
|
}
|
|
457
271
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -467,7 +281,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
467
281
|
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
468
282
|
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
469
283
|
}
|
|
470
|
-
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
284
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
|
|
471
285
|
return { ok: false, error: "email must be a valid email address" };
|
|
472
286
|
}
|
|
473
287
|
const acked = hasDisclaimerAck(fields);
|
|
@@ -483,7 +297,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
483
297
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
484
298
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
485
299
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
486
|
-
row.emailNormalized =
|
|
300
|
+
row.emailNormalized = normalizeChapterEmail(fields.email);
|
|
487
301
|
}
|
|
488
302
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
489
303
|
row.admissionGrantId = opts.admissionGrant.id;
|
|
@@ -806,6 +620,30 @@ var signupControls = {
|
|
|
806
620
|
receivedAt: attr4("number", { indexed: true })
|
|
807
621
|
}
|
|
808
622
|
};
|
|
623
|
+
var memberContentControls = {
|
|
624
|
+
attrs: {
|
|
625
|
+
id: id3(),
|
|
626
|
+
sourceId: attr4("string", { indexed: true }),
|
|
627
|
+
targetId: attr4("string", { indexed: true }),
|
|
628
|
+
environment: attr4("string", { indexed: true }),
|
|
629
|
+
revision: attr4("number", { indexed: true }),
|
|
630
|
+
digest: attr4("string", { indexed: true }),
|
|
631
|
+
document: attr4("json"),
|
|
632
|
+
active: attr4("boolean", { indexed: true }),
|
|
633
|
+
receivedAt: attr4("number", { indexed: true })
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
var memberContentLocal = {
|
|
637
|
+
attrs: {
|
|
638
|
+
id: id3(),
|
|
639
|
+
chapterId: attr4("string", { indexed: true }),
|
|
640
|
+
environment: attr4("string", { indexed: true }),
|
|
641
|
+
revision: attr4("number", { indexed: true }),
|
|
642
|
+
document: attr4("json"),
|
|
643
|
+
updatedAt: attr4("number", { indexed: true }),
|
|
644
|
+
updatedBy: attr4("string", { optional: true })
|
|
645
|
+
}
|
|
646
|
+
};
|
|
809
647
|
var groups = {
|
|
810
648
|
attrs: {
|
|
811
649
|
id: id3(),
|
|
@@ -877,7 +715,7 @@ var emailLog = {
|
|
|
877
715
|
sentAt: attr4("number", { indexed: true })
|
|
878
716
|
}
|
|
879
717
|
};
|
|
880
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
718
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
|
|
881
719
|
const entities = {};
|
|
882
720
|
if (mode === "chapter") {
|
|
883
721
|
entities.applications = applicationsFor(application);
|
|
@@ -893,6 +731,10 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
893
731
|
entities.giftEntitlements = giftEntitlementsEntity;
|
|
894
732
|
entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
|
|
895
733
|
if (includeSignupControl) entities.signupControls = signupControls;
|
|
734
|
+
if (includeMemberContent) {
|
|
735
|
+
entities.memberContentControls = memberContentControls;
|
|
736
|
+
entities.memberContentLocal = memberContentLocal;
|
|
737
|
+
}
|
|
896
738
|
}
|
|
897
739
|
if (auth.source === "table") entities.admins = admins;
|
|
898
740
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
@@ -1517,6 +1359,82 @@ function formatChapterCopy(template, values) {
|
|
|
1517
1359
|
);
|
|
1518
1360
|
}
|
|
1519
1361
|
|
|
1362
|
+
// src/flow-fields.ts
|
|
1363
|
+
var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
|
|
1364
|
+
var APPLICATION_LABELS = {
|
|
1365
|
+
firstName: "First name",
|
|
1366
|
+
lastName: "Last name",
|
|
1367
|
+
email: "Email",
|
|
1368
|
+
referral: "Referral source",
|
|
1369
|
+
referralName: "Referral name",
|
|
1370
|
+
whoYouAre: "Who you are",
|
|
1371
|
+
linkedin: "LinkedIn",
|
|
1372
|
+
message: "Intro message",
|
|
1373
|
+
phone: "Phone",
|
|
1374
|
+
state: "State"
|
|
1375
|
+
};
|
|
1376
|
+
function humanize(id4) {
|
|
1377
|
+
const text3 = id4.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1378
|
+
return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id4;
|
|
1379
|
+
}
|
|
1380
|
+
function applicationDefs(chapter) {
|
|
1381
|
+
try {
|
|
1382
|
+
return chapter.crm.type("person").fields;
|
|
1383
|
+
} catch {
|
|
1384
|
+
return {};
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
function applicationType(id4, def) {
|
|
1388
|
+
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
1389
|
+
return id4 === "email" ? "email" : id4 === "linkedin" ? "url" : "string";
|
|
1390
|
+
}
|
|
1391
|
+
function applicationFlowFields(chapter, application = chapter.application) {
|
|
1392
|
+
const defs = applicationDefs(chapter);
|
|
1393
|
+
return [...application.required, ...application.optional].map((id4, order) => {
|
|
1394
|
+
const def = defs[id4];
|
|
1395
|
+
const conditions = application.conditions[id4];
|
|
1396
|
+
const type = applicationType(id4, def);
|
|
1397
|
+
return {
|
|
1398
|
+
id: id4,
|
|
1399
|
+
label: def?.label ?? APPLICATION_LABELS[id4] ?? humanize(id4),
|
|
1400
|
+
type,
|
|
1401
|
+
required: application.required.includes(id4),
|
|
1402
|
+
order,
|
|
1403
|
+
maxLength: application.maxLen[id4] ?? application.defaultMaxLen,
|
|
1404
|
+
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
1405
|
+
...conditions ? { conditions: { ...conditions } } : {},
|
|
1406
|
+
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
1407
|
+
};
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
function resolveFormationFlowFields(crm, formation) {
|
|
1411
|
+
if (!formation.enabled) return [];
|
|
1412
|
+
const def = crm.type(formation.type);
|
|
1413
|
+
return [...formation.required, ...formation.optional].map((id4, order) => {
|
|
1414
|
+
const field = def.fields[id4];
|
|
1415
|
+
return {
|
|
1416
|
+
id: id4,
|
|
1417
|
+
label: field.label ?? humanize(id4),
|
|
1418
|
+
type: field.type,
|
|
1419
|
+
required: formation.required.includes(id4),
|
|
1420
|
+
order,
|
|
1421
|
+
maxLength: formation.maxLen[id4] ?? formation.defaultMaxLen,
|
|
1422
|
+
...field.options ? { options: [...field.options] } : {},
|
|
1423
|
+
...id4 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1424
|
+
};
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
function formationFlowFields(chapter, formation = chapter.formation) {
|
|
1428
|
+
return resolveFormationFlowFields(chapter.crm, formation);
|
|
1429
|
+
}
|
|
1430
|
+
function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
|
|
1431
|
+
return {
|
|
1432
|
+
schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
1433
|
+
flow,
|
|
1434
|
+
fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1520
1438
|
// src/formation.ts
|
|
1521
1439
|
var DEFAULT_REQUIRED2 = ["name", "email", "location", "thesis"];
|
|
1522
1440
|
var DEFAULT_OPTIONAL2 = ["proposedChapter", "experience", "collaborators"];
|
|
@@ -1579,17 +1497,7 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1579
1497
|
};
|
|
1580
1498
|
}
|
|
1581
1499
|
function formationFields(crm, formation) {
|
|
1582
|
-
|
|
1583
|
-
return [...formation.required, ...formation.optional].map((id4) => {
|
|
1584
|
-
const field = def.fields[id4];
|
|
1585
|
-
return {
|
|
1586
|
-
id: id4,
|
|
1587
|
-
label: field.label ?? id4,
|
|
1588
|
-
type: field.type,
|
|
1589
|
-
required: formation.required.includes(id4),
|
|
1590
|
-
...field.options ? { options: field.options } : {}
|
|
1591
|
-
};
|
|
1592
|
-
});
|
|
1500
|
+
return resolveFormationFlowFields(crm, formation);
|
|
1593
1501
|
}
|
|
1594
1502
|
|
|
1595
1503
|
// src/config-network.ts
|
|
@@ -1738,15 +1646,35 @@ function resolveSignupControl(input) {
|
|
|
1738
1646
|
return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
|
|
1739
1647
|
}
|
|
1740
1648
|
|
|
1741
|
-
// src/config.ts
|
|
1649
|
+
// src/config-member-content.ts
|
|
1742
1650
|
var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1651
|
+
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1652
|
+
function resolveMemberContent(input) {
|
|
1653
|
+
if (!input) return null;
|
|
1654
|
+
const parent = input.parent;
|
|
1655
|
+
if (parent && !SLUG3.test(parent.sourceId)) {
|
|
1656
|
+
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1657
|
+
}
|
|
1658
|
+
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
1659
|
+
if (parent && !SECRET_NAME2.test(secretName)) {
|
|
1660
|
+
throw new Error("defineChapter.memberContent.parent.secretName: must be a vault-key identifier");
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
enabled: true,
|
|
1664
|
+
localAuthoring: input.localAuthoring !== false,
|
|
1665
|
+
parent: parent ? { sourceId: parent.sourceId, secretName } : null
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/config.ts
|
|
1670
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1743
1671
|
function isResolvedCrm(x) {
|
|
1744
1672
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1745
1673
|
}
|
|
1746
1674
|
function defineChapter(config) {
|
|
1747
1675
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1748
1676
|
const { id: id4, name } = config;
|
|
1749
|
-
if (typeof id4 !== "string" || !
|
|
1677
|
+
if (typeof id4 !== "string" || !SLUG4.test(id4)) {
|
|
1750
1678
|
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1751
1679
|
}
|
|
1752
1680
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
@@ -1771,16 +1699,21 @@ function defineChapter(config) {
|
|
|
1771
1699
|
const copy = resolveChapterCopy(config.copy);
|
|
1772
1700
|
const network = resolveNetwork(config, crm);
|
|
1773
1701
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1702
|
+
const memberContent = resolveMemberContent(config.memberContent);
|
|
1774
1703
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1775
1704
|
if (signupControl && mode !== "chapter") {
|
|
1776
1705
|
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
1777
1706
|
}
|
|
1707
|
+
if (memberContent && mode !== "chapter") {
|
|
1708
|
+
throw new Error("defineChapter.memberContent: only chapter-mode sites can serve member content");
|
|
1709
|
+
}
|
|
1778
1710
|
const { schema, rules } = chapterDb(
|
|
1779
1711
|
mode,
|
|
1780
1712
|
auth,
|
|
1781
1713
|
network.targets.length > 0,
|
|
1782
1714
|
application,
|
|
1783
|
-
Boolean(signupControl)
|
|
1715
|
+
Boolean(signupControl),
|
|
1716
|
+
Boolean(memberContent)
|
|
1784
1717
|
);
|
|
1785
1718
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1786
1719
|
const account = config.account ?? "none";
|
|
@@ -1837,6 +1770,7 @@ function defineChapter(config) {
|
|
|
1837
1770
|
copy,
|
|
1838
1771
|
network,
|
|
1839
1772
|
signupControl,
|
|
1773
|
+
memberContent,
|
|
1840
1774
|
formation,
|
|
1841
1775
|
crm,
|
|
1842
1776
|
auth,
|
|
@@ -2099,44 +2033,364 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2099
2033
|
});
|
|
2100
2034
|
}
|
|
2101
2035
|
}
|
|
2102
|
-
const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
|
|
2103
|
-
const runbooks = isLeader ? {
|
|
2104
|
-
role: "leader",
|
|
2105
|
-
slugs: ["chapter-leader", "chapter-network", "crm"],
|
|
2106
|
-
guidance: `${chapter.name} is a LEADER hub: it holds the CRM and shares records with its chapters. Read it with \`odla-ai runbook get chapter-leader\`.`
|
|
2107
|
-
} : {
|
|
2108
|
-
role: "follower",
|
|
2109
|
-
slugs: ["chapter-follower", "chapter-network"],
|
|
2110
|
-
guidance: `${chapter.name} is a FOLLOWER site: it receives people and records from a leader hub. Read it with \`odla-ai runbook get chapter-follower\`.`
|
|
2036
|
+
const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
|
|
2037
|
+
const runbooks = isLeader ? {
|
|
2038
|
+
role: "leader",
|
|
2039
|
+
slugs: ["chapter-leader", "chapter-network", "crm"],
|
|
2040
|
+
guidance: `${chapter.name} is a LEADER hub: it holds the CRM and shares records with its chapters. Read it with \`odla-ai runbook get chapter-leader\`.`
|
|
2041
|
+
} : {
|
|
2042
|
+
role: "follower",
|
|
2043
|
+
slugs: ["chapter-follower", "chapter-network"],
|
|
2044
|
+
guidance: `${chapter.name} is a FOLLOWER site: it receives people and records from a leader hub. Read it with \`odla-ai runbook get chapter-follower\`.`
|
|
2045
|
+
};
|
|
2046
|
+
return {
|
|
2047
|
+
id: "chapter",
|
|
2048
|
+
title: `Chapter: ${chapter.name}`,
|
|
2049
|
+
npm: "@odla-ai/chapter",
|
|
2050
|
+
schema: {
|
|
2051
|
+
entities: { ...crmDesc.schema.entities, ...chapter.schema.entities },
|
|
2052
|
+
links: { ...crmDesc.schema.links, ...chapter.schema.links }
|
|
2053
|
+
},
|
|
2054
|
+
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
2055
|
+
seeds,
|
|
2056
|
+
probes: [
|
|
2057
|
+
...crmDesc.probes ?? [],
|
|
2058
|
+
// The snapshot route is deliberately private. A credential-free smoke
|
|
2059
|
+
// request must prove that the route is mounted and closed.
|
|
2060
|
+
{ path: "/api/network/snapshot", expectedStatus: 401 },
|
|
2061
|
+
...chapter.network.readers.length > 0 ? [{ path: "/api/network/records", expectedStatus: 401 }] : [],
|
|
2062
|
+
...chapter.network.readers.some((reader) => reader.sharedNotes.length > 0) ? [{
|
|
2063
|
+
path: "/api/network/shared-notes",
|
|
2064
|
+
expectedStatus: 401
|
|
2065
|
+
}] : [],
|
|
2066
|
+
// Formation metadata is public only when the host explicitly enables
|
|
2067
|
+
// the bounded intake contract.
|
|
2068
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
|
|
2069
|
+
...chapter.memberContent ? [
|
|
2070
|
+
{ path: "/api/member-content", expectedStatus: 401 },
|
|
2071
|
+
{ path: "/api/admin/member-content", expectedStatus: 401 }
|
|
2072
|
+
] : []
|
|
2073
|
+
],
|
|
2074
|
+
runbooks
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// src/member-content-validation-helpers.ts
|
|
2079
|
+
var ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2080
|
+
var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2081
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2082
|
+
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2083
|
+
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
2084
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
2085
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
2086
|
+
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
2087
|
+
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
2088
|
+
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
2089
|
+
var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
|
|
2090
|
+
var MAX_PAGES = 100;
|
|
2091
|
+
var MAX_MARKDOWN = 65536;
|
|
2092
|
+
var MAX_TEXT = 240;
|
|
2093
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2094
|
+
function unknownKeys(value, allowed2, path, errors) {
|
|
2095
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2096
|
+
}
|
|
2097
|
+
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
2098
|
+
if (value === void 0) {
|
|
2099
|
+
if (required) errors.push(`${path} is required`);
|
|
2100
|
+
return void 0;
|
|
2101
|
+
}
|
|
2102
|
+
if (typeof value !== "string") {
|
|
2103
|
+
errors.push(`${path} must be a string`);
|
|
2104
|
+
return void 0;
|
|
2105
|
+
}
|
|
2106
|
+
const valueText = value.trim();
|
|
2107
|
+
if (required && !valueText) errors.push(`${path} is required`);
|
|
2108
|
+
if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2109
|
+
return valueText;
|
|
2110
|
+
}
|
|
2111
|
+
function offerValue(value, path, errors, tiers2, audience) {
|
|
2112
|
+
const raw = record(value);
|
|
2113
|
+
if (!raw) {
|
|
2114
|
+
errors.push(`${path} must be an object`);
|
|
2115
|
+
return void 0;
|
|
2116
|
+
}
|
|
2117
|
+
unknownKeys(raw, OFFER_KEYS, path, errors);
|
|
2118
|
+
const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2119
|
+
if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2120
|
+
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2121
|
+
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2122
|
+
errors.push(`${path}.tierId must grant access to this page`);
|
|
2123
|
+
}
|
|
2124
|
+
const label = textValue(raw.label, `${path}.label`, errors);
|
|
2125
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
|
|
2126
|
+
const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2127
|
+
return {
|
|
2128
|
+
tierId,
|
|
2129
|
+
...label ? { label } : {},
|
|
2130
|
+
...bodyMarkdown ? { bodyMarkdown } : {},
|
|
2131
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// src/member-content-validation.ts
|
|
2136
|
+
function validateMemberContentDocument(input, options = {}) {
|
|
2137
|
+
const errors = [];
|
|
2138
|
+
const root = record(input);
|
|
2139
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2140
|
+
unknownKeys(root, ROOT_KEYS, "document", errors);
|
|
2141
|
+
if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2142
|
+
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
2143
|
+
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
2144
|
+
const sourceRaw = record(root.source);
|
|
2145
|
+
let source = { type: "chapter", appId: "invalid" };
|
|
2146
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2147
|
+
else {
|
|
2148
|
+
unknownKeys(sourceRaw, SOURCE_KEYS, "document.source", errors);
|
|
2149
|
+
const type = sourceRaw.type;
|
|
2150
|
+
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
2151
|
+
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
2152
|
+
if (appId && !ID.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2153
|
+
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2154
|
+
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2155
|
+
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
2156
|
+
}
|
|
2157
|
+
let target;
|
|
2158
|
+
if (root.target !== void 0) {
|
|
2159
|
+
const targetRaw = record(root.target);
|
|
2160
|
+
if (!targetRaw) errors.push("document.target must be an object");
|
|
2161
|
+
else {
|
|
2162
|
+
unknownKeys(targetRaw, TARGET_KEYS, "document.target", errors);
|
|
2163
|
+
const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
|
|
2164
|
+
const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
|
|
2165
|
+
target = { chapterId, environment };
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
|
|
2169
|
+
if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
|
|
2170
|
+
if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
|
|
2171
|
+
const rawPages = Array.isArray(root.pages) ? root.pages : [];
|
|
2172
|
+
if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
|
|
2173
|
+
const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
|
|
2174
|
+
const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
|
|
2175
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2176
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2177
|
+
const pages = [];
|
|
2178
|
+
for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2179
|
+
const path = `document.pages[${index}]`;
|
|
2180
|
+
const raw = record(value);
|
|
2181
|
+
if (!raw) {
|
|
2182
|
+
errors.push(`${path} must be an object`);
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
unknownKeys(raw, PAGE_KEYS, path, errors);
|
|
2186
|
+
const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2187
|
+
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2188
|
+
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2189
|
+
if (id4 && !ID.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2190
|
+
if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2191
|
+
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2192
|
+
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2193
|
+
ids.add(id4);
|
|
2194
|
+
slugs.add(slug);
|
|
2195
|
+
const navRaw = record(raw.navigation);
|
|
2196
|
+
let navigation = { section: "General", order: index };
|
|
2197
|
+
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2198
|
+
else {
|
|
2199
|
+
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2200
|
+
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2201
|
+
const order = navRaw.order;
|
|
2202
|
+
if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
|
|
2203
|
+
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2204
|
+
}
|
|
2205
|
+
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2206
|
+
navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
|
|
2207
|
+
}
|
|
2208
|
+
const audienceRaw = record(raw.audience);
|
|
2209
|
+
let audience = { kind: "all-members" };
|
|
2210
|
+
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2211
|
+
else {
|
|
2212
|
+
unknownKeys(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
|
|
2213
|
+
if (audienceRaw.kind === "all-members") {
|
|
2214
|
+
if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
|
|
2215
|
+
} else if (audienceRaw.kind === "tiers") {
|
|
2216
|
+
if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
|
|
2217
|
+
errors.push(`${path}.audience.tierIds must be a non-empty array`);
|
|
2218
|
+
}
|
|
2219
|
+
const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
|
|
2220
|
+
const normalized = [];
|
|
2221
|
+
for (const [tierIndex, value2] of values.entries()) {
|
|
2222
|
+
const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
|
|
2223
|
+
if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
|
|
2224
|
+
if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
|
|
2225
|
+
normalized.push(tier);
|
|
2226
|
+
}
|
|
2227
|
+
audience = { kind: "tiers", tierIds: normalized };
|
|
2228
|
+
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2229
|
+
}
|
|
2230
|
+
const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
|
|
2231
|
+
const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
|
|
2232
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
|
|
2233
|
+
let upsell;
|
|
2234
|
+
if (raw.upsell !== void 0) {
|
|
2235
|
+
const upsellRaw = record(raw.upsell);
|
|
2236
|
+
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2237
|
+
else {
|
|
2238
|
+
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
2239
|
+
const owner = upsellRaw.owner;
|
|
2240
|
+
if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
|
|
2241
|
+
if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
|
|
2242
|
+
if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
|
|
2243
|
+
if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
|
|
2244
|
+
const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
|
|
2245
|
+
if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
|
|
2246
|
+
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
pages.push({ id: id4, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2250
|
+
}
|
|
2251
|
+
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2252
|
+
for (const [index, page] of pages.entries()) {
|
|
2253
|
+
const parentId = page.navigation.parentId;
|
|
2254
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
|
|
2255
|
+
if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
|
|
2256
|
+
}
|
|
2257
|
+
for (const page of pages) {
|
|
2258
|
+
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
2259
|
+
let parentId = page.navigation.parentId;
|
|
2260
|
+
while (parentId) {
|
|
2261
|
+
if (seen.has(parentId)) {
|
|
2262
|
+
errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
|
|
2263
|
+
break;
|
|
2264
|
+
}
|
|
2265
|
+
seen.add(parentId);
|
|
2266
|
+
parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
let upsellOverrides;
|
|
2270
|
+
if (root.upsellOverrides !== void 0) {
|
|
2271
|
+
if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
|
|
2272
|
+
if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
|
|
2273
|
+
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2274
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2275
|
+
upsellOverrides = [];
|
|
2276
|
+
for (const [index, value] of values.entries()) {
|
|
2277
|
+
const path = `document.upsellOverrides[${index}]`;
|
|
2278
|
+
const raw = record(value);
|
|
2279
|
+
if (!raw) {
|
|
2280
|
+
errors.push(`${path} must be an object`);
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
unknownKeys(raw, OVERRIDE_KEYS, path, errors);
|
|
2284
|
+
const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2285
|
+
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2286
|
+
seen.add(pageId);
|
|
2287
|
+
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2288
|
+
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2289
|
+
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2290
|
+
const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2291
|
+
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
if (errors.length) return { ok: false, errors };
|
|
2295
|
+
return { ok: true, value: {
|
|
2296
|
+
schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
|
|
2297
|
+
revision: Number(root.revision),
|
|
2298
|
+
generatedAt: Number(root.generatedAt),
|
|
2299
|
+
source,
|
|
2300
|
+
...target ? { target } : {},
|
|
2301
|
+
pages,
|
|
2302
|
+
...upsellOverrides ? { upsellOverrides } : {}
|
|
2303
|
+
} };
|
|
2304
|
+
}
|
|
2305
|
+
function memberContentDocument(input, options = {}) {
|
|
2306
|
+
const result = validateMemberContentDocument(input, options);
|
|
2307
|
+
if (!result.ok) throw new TypeError(`Invalid member content document: ${result.errors.join("; ")}`);
|
|
2308
|
+
return result.value;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// src/member-content.ts
|
|
2312
|
+
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
2313
|
+
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
2314
|
+
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
2315
|
+
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);
|
|
2316
|
+
function composeMemberContent(parent, chapter) {
|
|
2317
|
+
const pages = [];
|
|
2318
|
+
const parentPages = /* @__PURE__ */ new Map();
|
|
2319
|
+
for (const page of parent?.pages ?? []) {
|
|
2320
|
+
const effective = { ...page, source: parent.source };
|
|
2321
|
+
parentPages.set(page.id, effective);
|
|
2322
|
+
pages.push(effective);
|
|
2323
|
+
}
|
|
2324
|
+
for (const override of chapter?.upsellOverrides ?? []) {
|
|
2325
|
+
const page = parentPages.get(override.pageId);
|
|
2326
|
+
if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
|
|
2327
|
+
}
|
|
2328
|
+
for (const page of chapter?.pages ?? []) {
|
|
2329
|
+
if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
|
|
2330
|
+
throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
|
|
2331
|
+
}
|
|
2332
|
+
pages.push({ ...page, source: chapter.source });
|
|
2333
|
+
}
|
|
2334
|
+
return {
|
|
2335
|
+
pages: pages.sort(pageOrder),
|
|
2336
|
+
parentRevision: parent?.revision ?? null,
|
|
2337
|
+
chapterRevision: chapter?.revision ?? null
|
|
2111
2338
|
};
|
|
2339
|
+
}
|
|
2340
|
+
var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
2341
|
+
function projectMemberContent(content, viewer, tiers2) {
|
|
2342
|
+
const authority = new Map(tiers2.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
2112
2343
|
return {
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
2135
|
-
],
|
|
2136
|
-
runbooks
|
|
2344
|
+
viewer: { ...viewer },
|
|
2345
|
+
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
2346
|
+
pages: content.pages.map((page) => {
|
|
2347
|
+
const canRead = allowed(page, viewer);
|
|
2348
|
+
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
2349
|
+
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
2350
|
+
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
2351
|
+
return {
|
|
2352
|
+
id: page.id,
|
|
2353
|
+
slug: page.slug,
|
|
2354
|
+
title: page.title,
|
|
2355
|
+
...page.summary ? { summary: page.summary } : {},
|
|
2356
|
+
navigation: page.navigation,
|
|
2357
|
+
audience: page.audience,
|
|
2358
|
+
source: page.source,
|
|
2359
|
+
access: canRead ? "allowed" : "locked",
|
|
2360
|
+
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
2361
|
+
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
2362
|
+
...upsell ? { upsell } : {}
|
|
2363
|
+
};
|
|
2364
|
+
})
|
|
2137
2365
|
};
|
|
2138
2366
|
}
|
|
2139
2367
|
|
|
2368
|
+
// src/member-content-canonical.ts
|
|
2369
|
+
var encoder = new TextEncoder();
|
|
2370
|
+
function canonicalValue(value) {
|
|
2371
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2372
|
+
return JSON.stringify(value);
|
|
2373
|
+
}
|
|
2374
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2375
|
+
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2376
|
+
const record3 = value;
|
|
2377
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2378
|
+
const item = record3[key];
|
|
2379
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2380
|
+
});
|
|
2381
|
+
return `{${entries.join(",")}}`;
|
|
2382
|
+
}
|
|
2383
|
+
function canonicalMemberContent(value) {
|
|
2384
|
+
return canonicalValue(value);
|
|
2385
|
+
}
|
|
2386
|
+
async function memberContentDigest(document) {
|
|
2387
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalMemberContent(document)));
|
|
2388
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2389
|
+
}
|
|
2390
|
+
async function createMemberContentEnvelope(document) {
|
|
2391
|
+
return { document, digest: await memberContentDigest(document) };
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2140
2394
|
// src/email.ts
|
|
2141
2395
|
function render(template, vars) {
|
|
2142
2396
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -2310,6 +2564,213 @@ function canceledPatch() {
|
|
|
2310
2564
|
return { canceled: true };
|
|
2311
2565
|
}
|
|
2312
2566
|
|
|
2567
|
+
// src/admission-grants-contract.ts
|
|
2568
|
+
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
2569
|
+
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
2570
|
+
|
|
2571
|
+
// src/signup-control-canonical.ts
|
|
2572
|
+
var encoder2 = new TextEncoder();
|
|
2573
|
+
function canonicalValue2(value) {
|
|
2574
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2575
|
+
return JSON.stringify(value);
|
|
2576
|
+
}
|
|
2577
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2578
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2579
|
+
const record3 = value;
|
|
2580
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2581
|
+
const item = record3[key];
|
|
2582
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2583
|
+
});
|
|
2584
|
+
return `{${entries.join(",")}}`;
|
|
2585
|
+
}
|
|
2586
|
+
function canonicalSignupControl(value) {
|
|
2587
|
+
return canonicalValue2(value);
|
|
2588
|
+
}
|
|
2589
|
+
async function signupControlDigest(document) {
|
|
2590
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalSignupControl(document)));
|
|
2591
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2592
|
+
}
|
|
2593
|
+
async function createSignupControlEnvelope(document) {
|
|
2594
|
+
return { document, digest: await signupControlDigest(document) };
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// src/admission-grants.ts
|
|
2598
|
+
var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
2599
|
+
var MAX_GRANTS = 2e3;
|
|
2600
|
+
var MAX_NOTE = 2e3;
|
|
2601
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2602
|
+
var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
|
|
2603
|
+
function normalizeAdmissionEmail(email) {
|
|
2604
|
+
return normalizeChapterEmail(email);
|
|
2605
|
+
}
|
|
2606
|
+
function validateAdmissionGrantDocument(input) {
|
|
2607
|
+
const errors = [];
|
|
2608
|
+
const root = input && typeof input === "object" ? input : {};
|
|
2609
|
+
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2610
|
+
const revision = Number(root.revision);
|
|
2611
|
+
const generatedAt = Number(root.generatedAt);
|
|
2612
|
+
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
2613
|
+
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
2614
|
+
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
2615
|
+
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
2616
|
+
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
2617
|
+
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
2618
|
+
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
2619
|
+
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
2620
|
+
if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
2621
|
+
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
2622
|
+
if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
2623
|
+
if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
2624
|
+
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
2625
|
+
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
2626
|
+
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
2627
|
+
const grants = [];
|
|
2628
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2629
|
+
const seenEmails = /* @__PURE__ */ new Set();
|
|
2630
|
+
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
2631
|
+
const grant = raw && typeof raw === "object" ? raw : {};
|
|
2632
|
+
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
2633
|
+
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
2634
|
+
const normalized = normalizeAdmissionEmail(email);
|
|
2635
|
+
const decision = grant.decision;
|
|
2636
|
+
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
2637
|
+
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
2638
|
+
if (!ID2.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
2639
|
+
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
2640
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
2641
|
+
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
2642
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
2643
|
+
errors.push(`document.grants[${index}].decision is invalid`);
|
|
2644
|
+
}
|
|
2645
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
2646
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
2647
|
+
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
2648
|
+
}
|
|
2649
|
+
seenIds.add(id4);
|
|
2650
|
+
seenEmails.add(normalized);
|
|
2651
|
+
grants.push({
|
|
2652
|
+
id: id4,
|
|
2653
|
+
email,
|
|
2654
|
+
decision,
|
|
2655
|
+
...note ? { note } : {},
|
|
2656
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
if (errors.length) return { ok: false, errors };
|
|
2660
|
+
return {
|
|
2661
|
+
ok: true,
|
|
2662
|
+
value: {
|
|
2663
|
+
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
2664
|
+
revision,
|
|
2665
|
+
generatedAt,
|
|
2666
|
+
source: { appId: sourceId, actor: sourceActor },
|
|
2667
|
+
target: { chapterId, environment },
|
|
2668
|
+
grants
|
|
2669
|
+
}
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
async function createAdmissionGrantEnvelope(document) {
|
|
2673
|
+
const checked = validateAdmissionGrantDocument(document);
|
|
2674
|
+
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
2675
|
+
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
2676
|
+
}
|
|
2677
|
+
var rows = (value) => Array.isArray(value) ? value : [];
|
|
2678
|
+
function asGrant(row) {
|
|
2679
|
+
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
2680
|
+
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
2681
|
+
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
2682
|
+
return row;
|
|
2683
|
+
}
|
|
2684
|
+
async function findAdmissionGrant(db, targetId, email, now) {
|
|
2685
|
+
const emailNormalized = normalizeAdmissionEmail(email);
|
|
2686
|
+
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
2687
|
+
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
2688
|
+
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
2689
|
+
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
2690
|
+
)).sort((a, b) => {
|
|
2691
|
+
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
2692
|
+
return scope || b.createdAt - a.createdAt;
|
|
2693
|
+
});
|
|
2694
|
+
return active[0] ?? null;
|
|
2695
|
+
}
|
|
2696
|
+
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
2697
|
+
const where = { sourceId, targetId, environment };
|
|
2698
|
+
if (db.aggregate) {
|
|
2699
|
+
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
2700
|
+
}
|
|
2701
|
+
const found = await db.query({ admissionGrantRevisions: {} });
|
|
2702
|
+
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
2703
|
+
}
|
|
2704
|
+
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
2705
|
+
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
2706
|
+
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
2707
|
+
where: { sourceId, targetId, environment },
|
|
2708
|
+
order: { revision: "desc" },
|
|
2709
|
+
limit: 1
|
|
2710
|
+
} } });
|
|
2711
|
+
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
2712
|
+
return row ? row : null;
|
|
2713
|
+
}
|
|
2714
|
+
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
2715
|
+
const { document, digest } = envelope;
|
|
2716
|
+
const current = rows((await db.query({ admissionGrants: { $: {
|
|
2717
|
+
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
2718
|
+
limit: MAX_GRANTS
|
|
2719
|
+
} } })).admissionGrants);
|
|
2720
|
+
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
2721
|
+
const ops = [];
|
|
2722
|
+
for (const row of current) {
|
|
2723
|
+
const id4 = String(row.id ?? "");
|
|
2724
|
+
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
2725
|
+
active: false,
|
|
2726
|
+
revokedAt: receivedAt,
|
|
2727
|
+
revokedBy: document.source.actor,
|
|
2728
|
+
updatedAt: receivedAt,
|
|
2729
|
+
updatedBy: document.source.actor
|
|
2730
|
+
} });
|
|
2731
|
+
}
|
|
2732
|
+
for (const grant of document.grants) {
|
|
2733
|
+
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
2734
|
+
const existing = current.find((row) => row.id === id4);
|
|
2735
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
2736
|
+
id: id4,
|
|
2737
|
+
sourceGrantId: grant.id,
|
|
2738
|
+
sourceScope: "parent",
|
|
2739
|
+
sourceId: document.source.appId,
|
|
2740
|
+
targetId: document.target.chapterId,
|
|
2741
|
+
email: grant.email,
|
|
2742
|
+
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
2743
|
+
decision: grant.decision,
|
|
2744
|
+
...grant.note ? { note: grant.note } : {},
|
|
2745
|
+
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
2746
|
+
active: true,
|
|
2747
|
+
sourceRevision: document.revision,
|
|
2748
|
+
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
2749
|
+
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
2750
|
+
updatedAt: receivedAt,
|
|
2751
|
+
updatedBy: document.source.actor
|
|
2752
|
+
} });
|
|
2753
|
+
const retract = [
|
|
2754
|
+
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
2755
|
+
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
2756
|
+
];
|
|
2757
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
2758
|
+
}
|
|
2759
|
+
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
2760
|
+
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
2761
|
+
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
2762
|
+
id: revisionId,
|
|
2763
|
+
sourceId: document.source.appId,
|
|
2764
|
+
targetId: document.target.chapterId,
|
|
2765
|
+
environment: document.target.environment,
|
|
2766
|
+
revision: document.revision,
|
|
2767
|
+
digest,
|
|
2768
|
+
active: true,
|
|
2769
|
+
receivedAt
|
|
2770
|
+
} });
|
|
2771
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2313
2774
|
// src/join-continuation.ts
|
|
2314
2775
|
function resolveJoinContinuation(input) {
|
|
2315
2776
|
if (input.status === "approved") return "accepted";
|
|
@@ -2331,10 +2792,10 @@ function resolveJoinContinuation(input) {
|
|
|
2331
2792
|
// src/gift-offers.ts
|
|
2332
2793
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2333
2794
|
var MAX_GIFT_OFFERS = 20;
|
|
2334
|
-
var
|
|
2795
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2335
2796
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2336
2797
|
var CURRENCY = /^[a-z]{3}$/;
|
|
2337
|
-
var
|
|
2798
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2338
2799
|
var text = (value, path, errors, max) => {
|
|
2339
2800
|
if (typeof value !== "string" || !value.trim()) {
|
|
2340
2801
|
errors.push(`${path} is required`);
|
|
@@ -2345,7 +2806,7 @@ var text = (value, path, errors, max) => {
|
|
|
2345
2806
|
};
|
|
2346
2807
|
function validateGiftOfferCatalog(input, tierIds) {
|
|
2347
2808
|
const errors = [];
|
|
2348
|
-
const root =
|
|
2809
|
+
const root = record2(input);
|
|
2349
2810
|
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2350
2811
|
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2351
2812
|
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
@@ -2356,18 +2817,18 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2356
2817
|
const offers = [];
|
|
2357
2818
|
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2358
2819
|
const path = `document.giftOffers.offers[${index}]`;
|
|
2359
|
-
const item =
|
|
2820
|
+
const item = record2(raw);
|
|
2360
2821
|
if (!item) {
|
|
2361
2822
|
errors.push(`${path} must be an object`);
|
|
2362
2823
|
continue;
|
|
2363
2824
|
}
|
|
2364
|
-
const
|
|
2365
|
-
for (const key of Object.keys(item)) if (!
|
|
2825
|
+
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2826
|
+
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2366
2827
|
const id4 = text(item.id, `${path}.id`, errors, 80);
|
|
2367
|
-
if (id4 && !
|
|
2828
|
+
if (id4 && !ID3.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2368
2829
|
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2369
2830
|
ids.add(id4);
|
|
2370
|
-
const targetRaw =
|
|
2831
|
+
const targetRaw = record2(item.target);
|
|
2371
2832
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2372
2833
|
if (targetRaw?.type === "tier") {
|
|
2373
2834
|
const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
@@ -2375,7 +2836,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2375
2836
|
target = { type: "tier", tierId };
|
|
2376
2837
|
} else if (targetRaw?.type === "entitlement") {
|
|
2377
2838
|
const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
2378
|
-
if (entitlementKey && !
|
|
2839
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2379
2840
|
target = { type: "entitlement", entitlementKey };
|
|
2380
2841
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2381
2842
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
@@ -2455,34 +2916,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
2455
2916
|
if (readable && raw.length <= 64) return raw;
|
|
2456
2917
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
2457
2918
|
}
|
|
2458
|
-
function normalizeSharedRecord(
|
|
2459
|
-
if ("input" in
|
|
2919
|
+
function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
2920
|
+
if ("input" in record3 && record3.version === 2) {
|
|
2460
2921
|
return {
|
|
2461
2922
|
version: 2,
|
|
2462
|
-
sourceId:
|
|
2463
|
-
sourceRecordId:
|
|
2464
|
-
type:
|
|
2465
|
-
input:
|
|
2923
|
+
sourceId: record3.source.siteId,
|
|
2924
|
+
sourceRecordId: record3.source.recordId,
|
|
2925
|
+
type: record3.type,
|
|
2926
|
+
input: record3.input
|
|
2466
2927
|
};
|
|
2467
2928
|
}
|
|
2468
|
-
if ("input" in
|
|
2929
|
+
if ("input" in record3) {
|
|
2469
2930
|
return {
|
|
2470
2931
|
version: 1,
|
|
2471
2932
|
sourceId: fallbackSourceId,
|
|
2472
|
-
sourceRecordId:
|
|
2473
|
-
type:
|
|
2474
|
-
input:
|
|
2933
|
+
sourceRecordId: record3.hubRecordId,
|
|
2934
|
+
type: record3.type,
|
|
2935
|
+
input: record3.input
|
|
2475
2936
|
};
|
|
2476
2937
|
}
|
|
2477
|
-
if ("type" in
|
|
2478
|
-
const input = { name:
|
|
2938
|
+
if ("type" in record3 && record3.type === "company") {
|
|
2939
|
+
const input = { name: record3.name };
|
|
2479
2940
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
2480
|
-
if (
|
|
2941
|
+
if (record3[key]) input[key] = record3[key];
|
|
2481
2942
|
}
|
|
2482
2943
|
return {
|
|
2483
2944
|
version: 1,
|
|
2484
2945
|
sourceId: fallbackSourceId,
|
|
2485
|
-
sourceRecordId:
|
|
2946
|
+
sourceRecordId: record3.hubRecordId,
|
|
2486
2947
|
type: "company",
|
|
2487
2948
|
input
|
|
2488
2949
|
};
|
|
@@ -2490,28 +2951,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
|
|
|
2490
2951
|
return {
|
|
2491
2952
|
version: 1,
|
|
2492
2953
|
sourceId: fallbackSourceId,
|
|
2493
|
-
sourceRecordId:
|
|
2954
|
+
sourceRecordId: record3.hubRecordId,
|
|
2494
2955
|
type: "person",
|
|
2495
|
-
input: sharedPersonInput(
|
|
2956
|
+
input: sharedPersonInput(record3)
|
|
2496
2957
|
};
|
|
2497
2958
|
}
|
|
2498
|
-
function sharedRecordFromCrm(crm,
|
|
2499
|
-
if (target.fields && !target.fields[
|
|
2500
|
-
throw new Error(`${target.name} does not accept "${
|
|
2959
|
+
function sharedRecordFromCrm(crm, record3, target, sourceId) {
|
|
2960
|
+
if (target.fields && !target.fields[record3.type]) {
|
|
2961
|
+
throw new Error(`${target.name} does not accept "${record3.type}" records`);
|
|
2501
2962
|
}
|
|
2502
|
-
const def = crm.type(
|
|
2503
|
-
const fields = target.fields?.[
|
|
2963
|
+
const def = crm.type(record3.type);
|
|
2964
|
+
const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
|
|
2504
2965
|
if (!fields) {
|
|
2505
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
2966
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
|
|
2506
2967
|
}
|
|
2507
2968
|
const nameField = def.nameField ?? "name";
|
|
2508
2969
|
const input = {};
|
|
2509
2970
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
2510
|
-
const value =
|
|
2971
|
+
const value = record3.fields?.[field];
|
|
2511
2972
|
if (value !== void 0) input[field] = value;
|
|
2512
2973
|
}
|
|
2513
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
2514
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
2974
|
+
if (input[nameField] === void 0) input[nameField] = record3.name;
|
|
2975
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
|
|
2515
2976
|
}
|
|
2516
2977
|
async function upsertPerson(deps, opts) {
|
|
2517
2978
|
const email = opts.email.toLowerCase();
|
|
@@ -2525,11 +2986,11 @@ async function upsertPerson(deps, opts) {
|
|
|
2525
2986
|
const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
2526
2987
|
return { recordId: created.id };
|
|
2527
2988
|
}
|
|
2528
|
-
async function findSharedRecord(deps,
|
|
2989
|
+
async function findSharedRecord(deps, record3, tag) {
|
|
2529
2990
|
const structured = await (0, import_crm5.getRecordByOrigin)(
|
|
2530
2991
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
2531
|
-
|
|
2532
|
-
|
|
2992
|
+
record3.sourceId,
|
|
2993
|
+
record3.sourceRecordId
|
|
2533
2994
|
);
|
|
2534
2995
|
if (structured) return structured;
|
|
2535
2996
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -2538,53 +2999,53 @@ async function findSharedRecord(deps, record2, tag) {
|
|
|
2538
2999
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
2539
3000
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2540
3001
|
}
|
|
2541
|
-
const def = deps.crm.type(
|
|
3002
|
+
const def = deps.crm.type(record3.type);
|
|
2542
3003
|
const emailField = def.emailField;
|
|
2543
|
-
if (emailField && typeof
|
|
2544
|
-
const primaryEmail =
|
|
3004
|
+
if (emailField && typeof record3.input[emailField] === "string") {
|
|
3005
|
+
const primaryEmail = record3.input[emailField].toLowerCase();
|
|
2545
3006
|
const found = await deps.db.query({
|
|
2546
|
-
crm_record: { $: { where: { type:
|
|
3007
|
+
crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
|
|
2547
3008
|
});
|
|
2548
3009
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2549
3010
|
}
|
|
2550
|
-
const domain =
|
|
3011
|
+
const domain = record3.input.domain;
|
|
2551
3012
|
const domainSlot = def.fields.domain?.slot;
|
|
2552
3013
|
if (typeof domain === "string" && domainSlot) {
|
|
2553
3014
|
const found = await deps.db.query({
|
|
2554
|
-
crm_record: { $: { where: { type:
|
|
3015
|
+
crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
|
|
2555
3016
|
});
|
|
2556
3017
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2557
3018
|
}
|
|
2558
3019
|
const nameField = def.nameField ?? "name";
|
|
2559
|
-
const name =
|
|
2560
|
-
if (
|
|
3020
|
+
const name = record3.input[nameField];
|
|
3021
|
+
if (record3.type === "company" && typeof name === "string" && name.trim()) {
|
|
2561
3022
|
const found = await deps.db.query({
|
|
2562
|
-
crm_record: { $: { where: { type:
|
|
3023
|
+
crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
|
|
2563
3024
|
});
|
|
2564
3025
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2565
3026
|
}
|
|
2566
3027
|
return void 0;
|
|
2567
3028
|
}
|
|
2568
3029
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
2569
|
-
const
|
|
2570
|
-
if (!
|
|
3030
|
+
const record3 = normalizeSharedRecord(shared, options.sourceId);
|
|
3031
|
+
if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
|
|
2571
3032
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
2572
3033
|
}
|
|
2573
|
-
if (options.sourceId && options.sourceId !==
|
|
3034
|
+
if (options.sourceId && options.sourceId !== record3.sourceId) {
|
|
2574
3035
|
throw new Error("signed sender does not match payload source");
|
|
2575
3036
|
}
|
|
2576
|
-
const tag = networkSourceTag(
|
|
3037
|
+
const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
|
|
2577
3038
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
2578
|
-
const existing = await findSharedRecord(deps,
|
|
3039
|
+
const existing = await findSharedRecord(deps, record3, tag);
|
|
2579
3040
|
let recordId;
|
|
2580
3041
|
if (existing && typeof existing.id === "string") {
|
|
2581
|
-
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input:
|
|
3042
|
+
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record3.input });
|
|
2582
3043
|
recordId = existing.id;
|
|
2583
3044
|
} else {
|
|
2584
|
-
recordId = `network_${shortHash(`${
|
|
3045
|
+
recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
|
|
2585
3046
|
await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
2586
|
-
type:
|
|
2587
|
-
input:
|
|
3047
|
+
type: record3.type,
|
|
3048
|
+
input: record3.input,
|
|
2588
3049
|
mutationId: `share-create:${tag}`
|
|
2589
3050
|
});
|
|
2590
3051
|
}
|
|
@@ -2594,10 +3055,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
2594
3055
|
);
|
|
2595
3056
|
await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
|
|
2596
3057
|
recordId,
|
|
2597
|
-
sourceId:
|
|
2598
|
-
sourceRecordId:
|
|
3058
|
+
sourceId: record3.sourceId,
|
|
3059
|
+
sourceRecordId: record3.sourceRecordId,
|
|
2599
3060
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
2600
|
-
payloadVersion:
|
|
3061
|
+
payloadVersion: record3.version
|
|
2601
3062
|
});
|
|
2602
3063
|
return { recordId };
|
|
2603
3064
|
}
|
|
@@ -2881,8 +3342,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
2881
3342
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
2882
3343
|
const page = Array.isArray(data) ? data : [];
|
|
2883
3344
|
for (const u of page) {
|
|
2884
|
-
const
|
|
2885
|
-
if (
|
|
3345
|
+
const record3 = toRecord(u);
|
|
3346
|
+
if (record3) out.push(record3);
|
|
2886
3347
|
}
|
|
2887
3348
|
if (page.length < PAGE) break;
|
|
2888
3349
|
}
|
|
@@ -2984,7 +3445,8 @@ function applicationSummary(app) {
|
|
|
2984
3445
|
meetingLink: app.meetingLink ?? null,
|
|
2985
3446
|
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
2986
3447
|
renewalAt: app.renewalAt ?? null,
|
|
2987
|
-
canceled: app.canceled === true
|
|
3448
|
+
canceled: app.canceled === true,
|
|
3449
|
+
tierId: app.tierId ?? null
|
|
2988
3450
|
};
|
|
2989
3451
|
}
|
|
2990
3452
|
function memberApplication(app, meeting, defaultTimezone) {
|
|
@@ -3288,7 +3750,7 @@ var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
|
3288
3750
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
3289
3751
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3290
3752
|
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
3291
|
-
var
|
|
3753
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
3292
3754
|
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
3293
3755
|
"tierId",
|
|
3294
3756
|
"variant",
|
|
@@ -3307,12 +3769,12 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
|
3307
3769
|
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
3308
3770
|
var MAX_TIERS = 20;
|
|
3309
3771
|
var MAX_BENEFITS = 24;
|
|
3310
|
-
var
|
|
3772
|
+
var MAX_MARKDOWN2 = 65536;
|
|
3311
3773
|
var MAX_LABEL = 160;
|
|
3312
3774
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3313
|
-
function rejectUnknownKeys(value,
|
|
3775
|
+
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
3314
3776
|
for (const key of Object.keys(value)) {
|
|
3315
|
-
if (!
|
|
3777
|
+
if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3316
3778
|
}
|
|
3317
3779
|
}
|
|
3318
3780
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -3333,7 +3795,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3333
3795
|
const errors = [];
|
|
3334
3796
|
const root = objectRecord(input);
|
|
3335
3797
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3336
|
-
rejectUnknownKeys(root,
|
|
3798
|
+
rejectUnknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
3337
3799
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3338
3800
|
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
3339
3801
|
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
@@ -3387,7 +3849,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3387
3849
|
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
3388
3850
|
else {
|
|
3389
3851
|
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
3390
|
-
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors,
|
|
3852
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN2, true);
|
|
3391
3853
|
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
3392
3854
|
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
3393
3855
|
}
|
|
@@ -3409,10 +3871,10 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3409
3871
|
}
|
|
3410
3872
|
}
|
|
3411
3873
|
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
3412
|
-
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors,
|
|
3874
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN2);
|
|
3413
3875
|
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
3414
3876
|
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
3415
|
-
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors,
|
|
3877
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN2);
|
|
3416
3878
|
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
3417
3879
|
tiers2.push({
|
|
3418
3880
|
tierId,
|
|
@@ -3434,7 +3896,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3434
3896
|
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
3435
3897
|
}
|
|
3436
3898
|
}
|
|
3437
|
-
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors,
|
|
3899
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN2);
|
|
3438
3900
|
if (errors.length) return { ok: false, errors };
|
|
3439
3901
|
return {
|
|
3440
3902
|
ok: true,
|
|
@@ -3453,11 +3915,11 @@ function membershipPageDocument(input, options = {}) {
|
|
|
3453
3915
|
}
|
|
3454
3916
|
|
|
3455
3917
|
// src/signup-control-validate.ts
|
|
3456
|
-
var
|
|
3918
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3457
3919
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
3458
3920
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3459
3921
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
3460
|
-
var
|
|
3922
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set([
|
|
3461
3923
|
"schemaVersion",
|
|
3462
3924
|
"revision",
|
|
3463
3925
|
"generatedAt",
|
|
@@ -3473,8 +3935,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
3473
3935
|
]);
|
|
3474
3936
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
3475
3937
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3476
|
-
function
|
|
3477
|
-
for (const key of Object.keys(value)) if (!
|
|
3938
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
3939
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3478
3940
|
}
|
|
3479
3941
|
function text2(value, path, errors, max = 65536) {
|
|
3480
3942
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -3498,7 +3960,7 @@ function tiersOf(value, errors) {
|
|
|
3498
3960
|
errors.push(`${path} must be an object`);
|
|
3499
3961
|
return [];
|
|
3500
3962
|
}
|
|
3501
|
-
|
|
3963
|
+
unknownKeys2(item, TIER_KEYS2, path, errors);
|
|
3502
3964
|
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
3503
3965
|
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3504
3966
|
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
@@ -3534,7 +3996,7 @@ function applicationOf(value, errors) {
|
|
|
3534
3996
|
errors.push("document.application must be an object");
|
|
3535
3997
|
return void 0;
|
|
3536
3998
|
}
|
|
3537
|
-
|
|
3999
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
3538
4000
|
try {
|
|
3539
4001
|
const resolved = resolveApplication(input);
|
|
3540
4002
|
const copy = recordOf(input.copy);
|
|
@@ -3559,10 +4021,10 @@ function policyOf(value, errors) {
|
|
|
3559
4021
|
errors.push("document.policy must be an object");
|
|
3560
4022
|
return void 0;
|
|
3561
4023
|
}
|
|
3562
|
-
const
|
|
3563
|
-
|
|
4024
|
+
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
4025
|
+
unknownKeys2(input, allowed2, "document.policy", errors);
|
|
3564
4026
|
const result = {};
|
|
3565
|
-
for (const key of
|
|
4027
|
+
for (const key of allowed2) {
|
|
3566
4028
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3567
4029
|
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
3568
4030
|
}
|
|
@@ -3576,7 +4038,7 @@ function themeOf(value, errors) {
|
|
|
3576
4038
|
errors.push("document.theme.tokens must be an object");
|
|
3577
4039
|
return void 0;
|
|
3578
4040
|
}
|
|
3579
|
-
|
|
4041
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
3580
4042
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
3581
4043
|
let serializedLength = 0;
|
|
3582
4044
|
for (const [key, token] of Object.entries(tokens)) {
|
|
@@ -3596,7 +4058,7 @@ function validateSignupControlDocument(input) {
|
|
|
3596
4058
|
const errors = [];
|
|
3597
4059
|
const root = recordOf(input);
|
|
3598
4060
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3599
|
-
|
|
4061
|
+
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
3600
4062
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3601
4063
|
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
3602
4064
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
@@ -3605,8 +4067,8 @@ function validateSignupControlDocument(input) {
|
|
|
3605
4067
|
const appId = text2(source?.appId, "document.source.appId", errors, 63);
|
|
3606
4068
|
const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
3607
4069
|
const environment = text2(target?.environment, "document.target.environment", errors, 63);
|
|
3608
|
-
if (appId && !
|
|
3609
|
-
if (chapterId && !
|
|
4070
|
+
if (appId && !SLUG6.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
4071
|
+
if (chapterId && !SLUG6.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3610
4072
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
3611
4073
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
3612
4074
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
@@ -3756,7 +4218,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
|
3756
4218
|
|
|
3757
4219
|
// src/membership-markdown.ts
|
|
3758
4220
|
var MAX_BENEFITS2 = 24;
|
|
3759
|
-
var
|
|
4221
|
+
var MAX_MARKDOWN3 = 65536;
|
|
3760
4222
|
var MAX_LABEL2 = 160;
|
|
3761
4223
|
var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
|
|
3762
4224
|
var MARKDOWN_HEADING = /^#{1,6}\s+/;
|
|
@@ -3790,8 +4252,8 @@ function markdownBenefits(lines, path, errors) {
|
|
|
3790
4252
|
}
|
|
3791
4253
|
function applyMembershipTierMarkdown(presentation, source) {
|
|
3792
4254
|
if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
|
|
3793
|
-
if (source.length >
|
|
3794
|
-
return { ok: false, errors: [`membership Markdown must be at most ${
|
|
4255
|
+
if (source.length > MAX_MARKDOWN3) {
|
|
4256
|
+
return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN3.toLocaleString("en-US")} characters`] };
|
|
3795
4257
|
}
|
|
3796
4258
|
if (!source.trim()) return { ok: true, value: { ...presentation } };
|
|
3797
4259
|
const errors = [];
|