@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.js
CHANGED
|
@@ -15,212 +15,10 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
15
15
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// src/
|
|
19
|
-
|
|
20
|
-
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
21
|
-
|
|
22
|
-
// src/signup-control-canonical.ts
|
|
23
|
-
var encoder = new TextEncoder();
|
|
24
|
-
function canonicalValue(value) {
|
|
25
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
26
|
-
return JSON.stringify(value);
|
|
27
|
-
}
|
|
28
|
-
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
29
|
-
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
30
|
-
const record2 = value;
|
|
31
|
-
const entries = Object.keys(record2).sort().flatMap((key) => {
|
|
32
|
-
const item = record2[key];
|
|
33
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
34
|
-
});
|
|
35
|
-
return `{${entries.join(",")}}`;
|
|
36
|
-
}
|
|
37
|
-
function canonicalSignupControl(value) {
|
|
38
|
-
return canonicalValue(value);
|
|
39
|
-
}
|
|
40
|
-
async function signupControlDigest(document) {
|
|
41
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
42
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
43
|
-
}
|
|
44
|
-
async function createSignupControlEnvelope(document) {
|
|
45
|
-
return { document, digest: await signupControlDigest(document) };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// src/admission-grants.ts
|
|
49
|
-
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
50
|
-
var MAX_GRANTS = 2e3;
|
|
51
|
-
var MAX_NOTE = 2e3;
|
|
52
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
53
|
-
var isAdmissionEmail = (value) => EMAIL_RE.test(value);
|
|
54
|
-
function normalizeAdmissionEmail(email) {
|
|
18
|
+
// src/email-identity.ts
|
|
19
|
+
function normalizeChapterEmail(email) {
|
|
55
20
|
return email.trim().toLowerCase();
|
|
56
21
|
}
|
|
57
|
-
function validateAdmissionGrantDocument(input) {
|
|
58
|
-
const errors = [];
|
|
59
|
-
const root = input && typeof input === "object" ? input : {};
|
|
60
|
-
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
61
|
-
const revision = Number(root.revision);
|
|
62
|
-
const generatedAt = Number(root.generatedAt);
|
|
63
|
-
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
64
|
-
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
65
|
-
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
66
|
-
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
67
|
-
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
68
|
-
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
69
|
-
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
70
|
-
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
71
|
-
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
72
|
-
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
73
|
-
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
74
|
-
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
75
|
-
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
76
|
-
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
77
|
-
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
78
|
-
const grants = [];
|
|
79
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
80
|
-
const seenEmails = /* @__PURE__ */ new Set();
|
|
81
|
-
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
82
|
-
const grant = raw && typeof raw === "object" ? raw : {};
|
|
83
|
-
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
84
|
-
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
85
|
-
const normalized = normalizeAdmissionEmail(email);
|
|
86
|
-
const decision = grant.decision;
|
|
87
|
-
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
88
|
-
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
89
|
-
if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
90
|
-
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
91
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
92
|
-
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
93
|
-
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
94
|
-
errors.push(`document.grants[${index}].decision is invalid`);
|
|
95
|
-
}
|
|
96
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
97
|
-
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
98
|
-
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
99
|
-
}
|
|
100
|
-
seenIds.add(id4);
|
|
101
|
-
seenEmails.add(normalized);
|
|
102
|
-
grants.push({
|
|
103
|
-
id: id4,
|
|
104
|
-
email,
|
|
105
|
-
decision,
|
|
106
|
-
...note ? { note } : {},
|
|
107
|
-
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
if (errors.length) return { ok: false, errors };
|
|
111
|
-
return {
|
|
112
|
-
ok: true,
|
|
113
|
-
value: {
|
|
114
|
-
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
115
|
-
revision,
|
|
116
|
-
generatedAt,
|
|
117
|
-
source: { appId: sourceId, actor: sourceActor },
|
|
118
|
-
target: { chapterId, environment },
|
|
119
|
-
grants
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
async function createAdmissionGrantEnvelope(document) {
|
|
124
|
-
const checked = validateAdmissionGrantDocument(document);
|
|
125
|
-
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
126
|
-
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
127
|
-
}
|
|
128
|
-
var rows = (value) => Array.isArray(value) ? value : [];
|
|
129
|
-
function asGrant(row) {
|
|
130
|
-
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
131
|
-
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
132
|
-
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
133
|
-
return row;
|
|
134
|
-
}
|
|
135
|
-
async function findAdmissionGrant(db, targetId, email, now) {
|
|
136
|
-
const emailNormalized = normalizeAdmissionEmail(email);
|
|
137
|
-
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
138
|
-
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
139
|
-
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
140
|
-
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
141
|
-
)).sort((a, b) => {
|
|
142
|
-
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
143
|
-
return scope || b.createdAt - a.createdAt;
|
|
144
|
-
});
|
|
145
|
-
return active[0] ?? null;
|
|
146
|
-
}
|
|
147
|
-
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
148
|
-
const where = { sourceId, targetId, environment };
|
|
149
|
-
if (db.aggregate) {
|
|
150
|
-
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
151
|
-
}
|
|
152
|
-
const found = await db.query({ admissionGrantRevisions: {} });
|
|
153
|
-
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
154
|
-
}
|
|
155
|
-
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
156
|
-
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
157
|
-
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
158
|
-
where: { sourceId, targetId, environment },
|
|
159
|
-
order: { revision: "desc" },
|
|
160
|
-
limit: 1
|
|
161
|
-
} } });
|
|
162
|
-
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
163
|
-
return row ? row : null;
|
|
164
|
-
}
|
|
165
|
-
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
166
|
-
const { document, digest } = envelope;
|
|
167
|
-
const current = rows((await db.query({ admissionGrants: { $: {
|
|
168
|
-
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
169
|
-
limit: MAX_GRANTS
|
|
170
|
-
} } })).admissionGrants);
|
|
171
|
-
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
172
|
-
const ops = [];
|
|
173
|
-
for (const row of current) {
|
|
174
|
-
const id4 = String(row.id ?? "");
|
|
175
|
-
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
176
|
-
active: false,
|
|
177
|
-
revokedAt: receivedAt,
|
|
178
|
-
revokedBy: document.source.actor,
|
|
179
|
-
updatedAt: receivedAt,
|
|
180
|
-
updatedBy: document.source.actor
|
|
181
|
-
} });
|
|
182
|
-
}
|
|
183
|
-
for (const grant of document.grants) {
|
|
184
|
-
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
185
|
-
const existing = current.find((row) => row.id === id4);
|
|
186
|
-
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
187
|
-
id: id4,
|
|
188
|
-
sourceGrantId: grant.id,
|
|
189
|
-
sourceScope: "parent",
|
|
190
|
-
sourceId: document.source.appId,
|
|
191
|
-
targetId: document.target.chapterId,
|
|
192
|
-
email: grant.email,
|
|
193
|
-
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
194
|
-
decision: grant.decision,
|
|
195
|
-
...grant.note ? { note: grant.note } : {},
|
|
196
|
-
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
197
|
-
active: true,
|
|
198
|
-
sourceRevision: document.revision,
|
|
199
|
-
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
200
|
-
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
201
|
-
updatedAt: receivedAt,
|
|
202
|
-
updatedBy: document.source.actor
|
|
203
|
-
} });
|
|
204
|
-
const retract = [
|
|
205
|
-
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
206
|
-
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
207
|
-
];
|
|
208
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
209
|
-
}
|
|
210
|
-
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
211
|
-
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
212
|
-
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
213
|
-
id: revisionId,
|
|
214
|
-
sourceId: document.source.appId,
|
|
215
|
-
targetId: document.target.chapterId,
|
|
216
|
-
environment: document.target.environment,
|
|
217
|
-
revision: document.revision,
|
|
218
|
-
digest,
|
|
219
|
-
active: true,
|
|
220
|
-
receivedAt
|
|
221
|
-
} });
|
|
222
|
-
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
223
|
-
}
|
|
224
22
|
|
|
225
23
|
// src/member.ts
|
|
226
24
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
@@ -273,9 +71,9 @@ function resolveApplication(a) {
|
|
|
273
71
|
validateEmail: a?.validateEmail ?? true
|
|
274
72
|
};
|
|
275
73
|
}
|
|
276
|
-
var
|
|
74
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
277
75
|
function isValidEmail(value) {
|
|
278
|
-
return typeof value === "string" &&
|
|
76
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
279
77
|
}
|
|
280
78
|
function clampArray(value, max) {
|
|
281
79
|
if (!Array.isArray(value)) return value;
|
|
@@ -287,14 +85,14 @@ function hasDisclaimerAck(fields) {
|
|
|
287
85
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
288
86
|
function applicantProfile(chapter, fields) {
|
|
289
87
|
const app = chapter.application;
|
|
290
|
-
const
|
|
88
|
+
const allowed2 = (f) => app.profileFields.includes(f);
|
|
291
89
|
const profile = {};
|
|
292
90
|
for (const f of [...app.required, ...app.optional]) {
|
|
293
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
91
|
+
if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
|
|
294
92
|
const v = fields[f];
|
|
295
93
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
296
94
|
}
|
|
297
|
-
if (fields.focus !== void 0 &&
|
|
95
|
+
if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
298
96
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
299
97
|
}
|
|
300
98
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -310,7 +108,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
310
108
|
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
311
109
|
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
312
110
|
}
|
|
313
|
-
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
111
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
|
|
314
112
|
return { ok: false, error: "email must be a valid email address" };
|
|
315
113
|
}
|
|
316
114
|
const acked = hasDisclaimerAck(fields);
|
|
@@ -326,7 +124,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
326
124
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
327
125
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
328
126
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
329
|
-
row.emailNormalized =
|
|
127
|
+
row.emailNormalized = normalizeChapterEmail(fields.email);
|
|
330
128
|
}
|
|
331
129
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
332
130
|
row.admissionGrantId = opts.admissionGrant.id;
|
|
@@ -649,6 +447,30 @@ var signupControls = {
|
|
|
649
447
|
receivedAt: attr4("number", { indexed: true })
|
|
650
448
|
}
|
|
651
449
|
};
|
|
450
|
+
var memberContentControls = {
|
|
451
|
+
attrs: {
|
|
452
|
+
id: id3(),
|
|
453
|
+
sourceId: attr4("string", { indexed: true }),
|
|
454
|
+
targetId: attr4("string", { indexed: true }),
|
|
455
|
+
environment: attr4("string", { indexed: true }),
|
|
456
|
+
revision: attr4("number", { indexed: true }),
|
|
457
|
+
digest: attr4("string", { indexed: true }),
|
|
458
|
+
document: attr4("json"),
|
|
459
|
+
active: attr4("boolean", { indexed: true }),
|
|
460
|
+
receivedAt: attr4("number", { indexed: true })
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
var memberContentLocal = {
|
|
464
|
+
attrs: {
|
|
465
|
+
id: id3(),
|
|
466
|
+
chapterId: attr4("string", { indexed: true }),
|
|
467
|
+
environment: attr4("string", { indexed: true }),
|
|
468
|
+
revision: attr4("number", { indexed: true }),
|
|
469
|
+
document: attr4("json"),
|
|
470
|
+
updatedAt: attr4("number", { indexed: true }),
|
|
471
|
+
updatedBy: attr4("string", { optional: true })
|
|
472
|
+
}
|
|
473
|
+
};
|
|
652
474
|
var groups = {
|
|
653
475
|
attrs: {
|
|
654
476
|
id: id3(),
|
|
@@ -720,7 +542,7 @@ var emailLog = {
|
|
|
720
542
|
sentAt: attr4("number", { indexed: true })
|
|
721
543
|
}
|
|
722
544
|
};
|
|
723
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
545
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
|
|
724
546
|
const entities = {};
|
|
725
547
|
if (mode === "chapter") {
|
|
726
548
|
entities.applications = applicationsFor(application);
|
|
@@ -736,6 +558,10 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
736
558
|
entities.giftEntitlements = giftEntitlementsEntity;
|
|
737
559
|
entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
|
|
738
560
|
if (includeSignupControl) entities.signupControls = signupControls;
|
|
561
|
+
if (includeMemberContent) {
|
|
562
|
+
entities.memberContentControls = memberContentControls;
|
|
563
|
+
entities.memberContentLocal = memberContentLocal;
|
|
564
|
+
}
|
|
739
565
|
}
|
|
740
566
|
if (auth.source === "table") entities.admins = admins;
|
|
741
567
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
@@ -1360,6 +1186,82 @@ function formatChapterCopy(template, values) {
|
|
|
1360
1186
|
);
|
|
1361
1187
|
}
|
|
1362
1188
|
|
|
1189
|
+
// src/flow-fields.ts
|
|
1190
|
+
var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
|
|
1191
|
+
var APPLICATION_LABELS = {
|
|
1192
|
+
firstName: "First name",
|
|
1193
|
+
lastName: "Last name",
|
|
1194
|
+
email: "Email",
|
|
1195
|
+
referral: "Referral source",
|
|
1196
|
+
referralName: "Referral name",
|
|
1197
|
+
whoYouAre: "Who you are",
|
|
1198
|
+
linkedin: "LinkedIn",
|
|
1199
|
+
message: "Intro message",
|
|
1200
|
+
phone: "Phone",
|
|
1201
|
+
state: "State"
|
|
1202
|
+
};
|
|
1203
|
+
function humanize(id4) {
|
|
1204
|
+
const text3 = id4.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1205
|
+
return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id4;
|
|
1206
|
+
}
|
|
1207
|
+
function applicationDefs(chapter) {
|
|
1208
|
+
try {
|
|
1209
|
+
return chapter.crm.type("person").fields;
|
|
1210
|
+
} catch {
|
|
1211
|
+
return {};
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function applicationType(id4, def) {
|
|
1215
|
+
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
1216
|
+
return id4 === "email" ? "email" : id4 === "linkedin" ? "url" : "string";
|
|
1217
|
+
}
|
|
1218
|
+
function applicationFlowFields(chapter, application = chapter.application) {
|
|
1219
|
+
const defs = applicationDefs(chapter);
|
|
1220
|
+
return [...application.required, ...application.optional].map((id4, order) => {
|
|
1221
|
+
const def = defs[id4];
|
|
1222
|
+
const conditions = application.conditions[id4];
|
|
1223
|
+
const type = applicationType(id4, def);
|
|
1224
|
+
return {
|
|
1225
|
+
id: id4,
|
|
1226
|
+
label: def?.label ?? APPLICATION_LABELS[id4] ?? humanize(id4),
|
|
1227
|
+
type,
|
|
1228
|
+
required: application.required.includes(id4),
|
|
1229
|
+
order,
|
|
1230
|
+
maxLength: application.maxLen[id4] ?? application.defaultMaxLen,
|
|
1231
|
+
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
1232
|
+
...conditions ? { conditions: { ...conditions } } : {},
|
|
1233
|
+
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
1234
|
+
};
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
function resolveFormationFlowFields(crm, formation) {
|
|
1238
|
+
if (!formation.enabled) return [];
|
|
1239
|
+
const def = crm.type(formation.type);
|
|
1240
|
+
return [...formation.required, ...formation.optional].map((id4, order) => {
|
|
1241
|
+
const field = def.fields[id4];
|
|
1242
|
+
return {
|
|
1243
|
+
id: id4,
|
|
1244
|
+
label: field.label ?? humanize(id4),
|
|
1245
|
+
type: field.type,
|
|
1246
|
+
required: formation.required.includes(id4),
|
|
1247
|
+
order,
|
|
1248
|
+
maxLength: formation.maxLen[id4] ?? formation.defaultMaxLen,
|
|
1249
|
+
...field.options ? { options: [...field.options] } : {},
|
|
1250
|
+
...id4 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1251
|
+
};
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
function formationFlowFields(chapter, formation = chapter.formation) {
|
|
1255
|
+
return resolveFormationFlowFields(chapter.crm, formation);
|
|
1256
|
+
}
|
|
1257
|
+
function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
|
|
1258
|
+
return {
|
|
1259
|
+
schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
1260
|
+
flow,
|
|
1261
|
+
fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1363
1265
|
// src/formation.ts
|
|
1364
1266
|
var DEFAULT_REQUIRED2 = ["name", "email", "location", "thesis"];
|
|
1365
1267
|
var DEFAULT_OPTIONAL2 = ["proposedChapter", "experience", "collaborators"];
|
|
@@ -1422,17 +1324,7 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1422
1324
|
};
|
|
1423
1325
|
}
|
|
1424
1326
|
function formationFields(crm, formation) {
|
|
1425
|
-
|
|
1426
|
-
return [...formation.required, ...formation.optional].map((id4) => {
|
|
1427
|
-
const field = def.fields[id4];
|
|
1428
|
-
return {
|
|
1429
|
-
id: id4,
|
|
1430
|
-
label: field.label ?? id4,
|
|
1431
|
-
type: field.type,
|
|
1432
|
-
required: formation.required.includes(id4),
|
|
1433
|
-
...field.options ? { options: field.options } : {}
|
|
1434
|
-
};
|
|
1435
|
-
});
|
|
1327
|
+
return resolveFormationFlowFields(crm, formation);
|
|
1436
1328
|
}
|
|
1437
1329
|
|
|
1438
1330
|
// src/config-network.ts
|
|
@@ -1581,15 +1473,35 @@ function resolveSignupControl(input) {
|
|
|
1581
1473
|
return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
|
|
1582
1474
|
}
|
|
1583
1475
|
|
|
1584
|
-
// src/config.ts
|
|
1476
|
+
// src/config-member-content.ts
|
|
1585
1477
|
var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1478
|
+
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1479
|
+
function resolveMemberContent(input) {
|
|
1480
|
+
if (!input) return null;
|
|
1481
|
+
const parent = input.parent;
|
|
1482
|
+
if (parent && !SLUG3.test(parent.sourceId)) {
|
|
1483
|
+
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1484
|
+
}
|
|
1485
|
+
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
1486
|
+
if (parent && !SECRET_NAME2.test(secretName)) {
|
|
1487
|
+
throw new Error("defineChapter.memberContent.parent.secretName: must be a vault-key identifier");
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
enabled: true,
|
|
1491
|
+
localAuthoring: input.localAuthoring !== false,
|
|
1492
|
+
parent: parent ? { sourceId: parent.sourceId, secretName } : null
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/config.ts
|
|
1497
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1586
1498
|
function isResolvedCrm(x) {
|
|
1587
1499
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1588
1500
|
}
|
|
1589
1501
|
function defineChapter(config) {
|
|
1590
1502
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1591
1503
|
const { id: id4, name } = config;
|
|
1592
|
-
if (typeof id4 !== "string" || !
|
|
1504
|
+
if (typeof id4 !== "string" || !SLUG4.test(id4)) {
|
|
1593
1505
|
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1594
1506
|
}
|
|
1595
1507
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
@@ -1614,16 +1526,21 @@ function defineChapter(config) {
|
|
|
1614
1526
|
const copy = resolveChapterCopy(config.copy);
|
|
1615
1527
|
const network = resolveNetwork(config, crm);
|
|
1616
1528
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1529
|
+
const memberContent = resolveMemberContent(config.memberContent);
|
|
1617
1530
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1618
1531
|
if (signupControl && mode !== "chapter") {
|
|
1619
1532
|
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
1620
1533
|
}
|
|
1534
|
+
if (memberContent && mode !== "chapter") {
|
|
1535
|
+
throw new Error("defineChapter.memberContent: only chapter-mode sites can serve member content");
|
|
1536
|
+
}
|
|
1621
1537
|
const { schema, rules } = chapterDb(
|
|
1622
1538
|
mode,
|
|
1623
1539
|
auth,
|
|
1624
1540
|
network.targets.length > 0,
|
|
1625
1541
|
application,
|
|
1626
|
-
Boolean(signupControl)
|
|
1542
|
+
Boolean(signupControl),
|
|
1543
|
+
Boolean(memberContent)
|
|
1627
1544
|
);
|
|
1628
1545
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1629
1546
|
const account = config.account ?? "none";
|
|
@@ -1680,6 +1597,7 @@ function defineChapter(config) {
|
|
|
1680
1597
|
copy,
|
|
1681
1598
|
network,
|
|
1682
1599
|
signupControl,
|
|
1600
|
+
memberContent,
|
|
1683
1601
|
formation,
|
|
1684
1602
|
crm,
|
|
1685
1603
|
auth,
|
|
@@ -1942,44 +1860,364 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1942
1860
|
});
|
|
1943
1861
|
}
|
|
1944
1862
|
}
|
|
1945
|
-
const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
|
|
1946
|
-
const runbooks = isLeader ? {
|
|
1947
|
-
role: "leader",
|
|
1948
|
-
slugs: ["chapter-leader", "chapter-network", "crm"],
|
|
1949
|
-
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\`.`
|
|
1950
|
-
} : {
|
|
1951
|
-
role: "follower",
|
|
1952
|
-
slugs: ["chapter-follower", "chapter-network"],
|
|
1953
|
-
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\`.`
|
|
1863
|
+
const isLeader = (chapter.config.network?.targets?.length ?? 0) > 0;
|
|
1864
|
+
const runbooks = isLeader ? {
|
|
1865
|
+
role: "leader",
|
|
1866
|
+
slugs: ["chapter-leader", "chapter-network", "crm"],
|
|
1867
|
+
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\`.`
|
|
1868
|
+
} : {
|
|
1869
|
+
role: "follower",
|
|
1870
|
+
slugs: ["chapter-follower", "chapter-network"],
|
|
1871
|
+
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\`.`
|
|
1872
|
+
};
|
|
1873
|
+
return {
|
|
1874
|
+
id: "chapter",
|
|
1875
|
+
title: `Chapter: ${chapter.name}`,
|
|
1876
|
+
npm: "@odla-ai/chapter",
|
|
1877
|
+
schema: {
|
|
1878
|
+
entities: { ...crmDesc.schema.entities, ...chapter.schema.entities },
|
|
1879
|
+
links: { ...crmDesc.schema.links, ...chapter.schema.links }
|
|
1880
|
+
},
|
|
1881
|
+
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
1882
|
+
seeds,
|
|
1883
|
+
probes: [
|
|
1884
|
+
...crmDesc.probes ?? [],
|
|
1885
|
+
// The snapshot route is deliberately private. A credential-free smoke
|
|
1886
|
+
// request must prove that the route is mounted and closed.
|
|
1887
|
+
{ path: "/api/network/snapshot", expectedStatus: 401 },
|
|
1888
|
+
...chapter.network.readers.length > 0 ? [{ path: "/api/network/records", expectedStatus: 401 }] : [],
|
|
1889
|
+
...chapter.network.readers.some((reader) => reader.sharedNotes.length > 0) ? [{
|
|
1890
|
+
path: "/api/network/shared-notes",
|
|
1891
|
+
expectedStatus: 401
|
|
1892
|
+
}] : [],
|
|
1893
|
+
// Formation metadata is public only when the host explicitly enables
|
|
1894
|
+
// the bounded intake contract.
|
|
1895
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
|
|
1896
|
+
...chapter.memberContent ? [
|
|
1897
|
+
{ path: "/api/member-content", expectedStatus: 401 },
|
|
1898
|
+
{ path: "/api/admin/member-content", expectedStatus: 401 }
|
|
1899
|
+
] : []
|
|
1900
|
+
],
|
|
1901
|
+
runbooks
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/member-content-validation-helpers.ts
|
|
1906
|
+
var ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1907
|
+
var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1908
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
1909
|
+
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
1910
|
+
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
1911
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
1912
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
1913
|
+
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
1914
|
+
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
1915
|
+
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
1916
|
+
var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
|
|
1917
|
+
var MAX_PAGES = 100;
|
|
1918
|
+
var MAX_MARKDOWN = 65536;
|
|
1919
|
+
var MAX_TEXT = 240;
|
|
1920
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1921
|
+
function unknownKeys(value, allowed2, path, errors) {
|
|
1922
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
1923
|
+
}
|
|
1924
|
+
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
1925
|
+
if (value === void 0) {
|
|
1926
|
+
if (required) errors.push(`${path} is required`);
|
|
1927
|
+
return void 0;
|
|
1928
|
+
}
|
|
1929
|
+
if (typeof value !== "string") {
|
|
1930
|
+
errors.push(`${path} must be a string`);
|
|
1931
|
+
return void 0;
|
|
1932
|
+
}
|
|
1933
|
+
const valueText = value.trim();
|
|
1934
|
+
if (required && !valueText) errors.push(`${path} is required`);
|
|
1935
|
+
if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
1936
|
+
return valueText;
|
|
1937
|
+
}
|
|
1938
|
+
function offerValue(value, path, errors, tiers2, audience) {
|
|
1939
|
+
const raw = record(value);
|
|
1940
|
+
if (!raw) {
|
|
1941
|
+
errors.push(`${path} must be an object`);
|
|
1942
|
+
return void 0;
|
|
1943
|
+
}
|
|
1944
|
+
unknownKeys(raw, OFFER_KEYS, path, errors);
|
|
1945
|
+
const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
1946
|
+
if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
1947
|
+
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
1948
|
+
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
1949
|
+
errors.push(`${path}.tierId must grant access to this page`);
|
|
1950
|
+
}
|
|
1951
|
+
const label = textValue(raw.label, `${path}.label`, errors);
|
|
1952
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
|
|
1953
|
+
const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
|
|
1954
|
+
return {
|
|
1955
|
+
tierId,
|
|
1956
|
+
...label ? { label } : {},
|
|
1957
|
+
...bodyMarkdown ? { bodyMarkdown } : {},
|
|
1958
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// src/member-content-validation.ts
|
|
1963
|
+
function validateMemberContentDocument(input, options = {}) {
|
|
1964
|
+
const errors = [];
|
|
1965
|
+
const root = record(input);
|
|
1966
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
1967
|
+
unknownKeys(root, ROOT_KEYS, "document", errors);
|
|
1968
|
+
if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
1969
|
+
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
1970
|
+
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
1971
|
+
const sourceRaw = record(root.source);
|
|
1972
|
+
let source = { type: "chapter", appId: "invalid" };
|
|
1973
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
1974
|
+
else {
|
|
1975
|
+
unknownKeys(sourceRaw, SOURCE_KEYS, "document.source", errors);
|
|
1976
|
+
const type = sourceRaw.type;
|
|
1977
|
+
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
1978
|
+
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
1979
|
+
if (appId && !ID.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
1980
|
+
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
1981
|
+
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
1982
|
+
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
1983
|
+
}
|
|
1984
|
+
let target;
|
|
1985
|
+
if (root.target !== void 0) {
|
|
1986
|
+
const targetRaw = record(root.target);
|
|
1987
|
+
if (!targetRaw) errors.push("document.target must be an object");
|
|
1988
|
+
else {
|
|
1989
|
+
unknownKeys(targetRaw, TARGET_KEYS, "document.target", errors);
|
|
1990
|
+
const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
|
|
1991
|
+
const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
|
|
1992
|
+
target = { chapterId, environment };
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
|
|
1996
|
+
if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
|
|
1997
|
+
if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
|
|
1998
|
+
const rawPages = Array.isArray(root.pages) ? root.pages : [];
|
|
1999
|
+
if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
|
|
2000
|
+
const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
|
|
2001
|
+
const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
|
|
2002
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2003
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2004
|
+
const pages = [];
|
|
2005
|
+
for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2006
|
+
const path = `document.pages[${index}]`;
|
|
2007
|
+
const raw = record(value);
|
|
2008
|
+
if (!raw) {
|
|
2009
|
+
errors.push(`${path} must be an object`);
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
unknownKeys(raw, PAGE_KEYS, path, errors);
|
|
2013
|
+
const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2014
|
+
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2015
|
+
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2016
|
+
if (id4 && !ID.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2017
|
+
if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2018
|
+
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2019
|
+
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2020
|
+
ids.add(id4);
|
|
2021
|
+
slugs.add(slug);
|
|
2022
|
+
const navRaw = record(raw.navigation);
|
|
2023
|
+
let navigation = { section: "General", order: index };
|
|
2024
|
+
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2025
|
+
else {
|
|
2026
|
+
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2027
|
+
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2028
|
+
const order = navRaw.order;
|
|
2029
|
+
if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
|
|
2030
|
+
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2031
|
+
}
|
|
2032
|
+
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2033
|
+
navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
|
|
2034
|
+
}
|
|
2035
|
+
const audienceRaw = record(raw.audience);
|
|
2036
|
+
let audience = { kind: "all-members" };
|
|
2037
|
+
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2038
|
+
else {
|
|
2039
|
+
unknownKeys(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
|
|
2040
|
+
if (audienceRaw.kind === "all-members") {
|
|
2041
|
+
if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
|
|
2042
|
+
} else if (audienceRaw.kind === "tiers") {
|
|
2043
|
+
if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
|
|
2044
|
+
errors.push(`${path}.audience.tierIds must be a non-empty array`);
|
|
2045
|
+
}
|
|
2046
|
+
const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
|
|
2047
|
+
const normalized = [];
|
|
2048
|
+
for (const [tierIndex, value2] of values.entries()) {
|
|
2049
|
+
const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
|
|
2050
|
+
if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
|
|
2051
|
+
if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
|
|
2052
|
+
normalized.push(tier);
|
|
2053
|
+
}
|
|
2054
|
+
audience = { kind: "tiers", tierIds: normalized };
|
|
2055
|
+
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2056
|
+
}
|
|
2057
|
+
const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
|
|
2058
|
+
const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
|
|
2059
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
|
|
2060
|
+
let upsell;
|
|
2061
|
+
if (raw.upsell !== void 0) {
|
|
2062
|
+
const upsellRaw = record(raw.upsell);
|
|
2063
|
+
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2064
|
+
else {
|
|
2065
|
+
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
2066
|
+
const owner = upsellRaw.owner;
|
|
2067
|
+
if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
|
|
2068
|
+
if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
|
|
2069
|
+
if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
|
|
2070
|
+
if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
|
|
2071
|
+
const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
|
|
2072
|
+
if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
|
|
2073
|
+
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
pages.push({ id: id4, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2077
|
+
}
|
|
2078
|
+
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2079
|
+
for (const [index, page] of pages.entries()) {
|
|
2080
|
+
const parentId = page.navigation.parentId;
|
|
2081
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
|
|
2082
|
+
if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
|
|
2083
|
+
}
|
|
2084
|
+
for (const page of pages) {
|
|
2085
|
+
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
2086
|
+
let parentId = page.navigation.parentId;
|
|
2087
|
+
while (parentId) {
|
|
2088
|
+
if (seen.has(parentId)) {
|
|
2089
|
+
errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
seen.add(parentId);
|
|
2093
|
+
parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
let upsellOverrides;
|
|
2097
|
+
if (root.upsellOverrides !== void 0) {
|
|
2098
|
+
if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
|
|
2099
|
+
if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
|
|
2100
|
+
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2101
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2102
|
+
upsellOverrides = [];
|
|
2103
|
+
for (const [index, value] of values.entries()) {
|
|
2104
|
+
const path = `document.upsellOverrides[${index}]`;
|
|
2105
|
+
const raw = record(value);
|
|
2106
|
+
if (!raw) {
|
|
2107
|
+
errors.push(`${path} must be an object`);
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
unknownKeys(raw, OVERRIDE_KEYS, path, errors);
|
|
2111
|
+
const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2112
|
+
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2113
|
+
seen.add(pageId);
|
|
2114
|
+
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2115
|
+
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2116
|
+
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2117
|
+
const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2118
|
+
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (errors.length) return { ok: false, errors };
|
|
2122
|
+
return { ok: true, value: {
|
|
2123
|
+
schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
|
|
2124
|
+
revision: Number(root.revision),
|
|
2125
|
+
generatedAt: Number(root.generatedAt),
|
|
2126
|
+
source,
|
|
2127
|
+
...target ? { target } : {},
|
|
2128
|
+
pages,
|
|
2129
|
+
...upsellOverrides ? { upsellOverrides } : {}
|
|
2130
|
+
} };
|
|
2131
|
+
}
|
|
2132
|
+
function memberContentDocument(input, options = {}) {
|
|
2133
|
+
const result = validateMemberContentDocument(input, options);
|
|
2134
|
+
if (!result.ok) throw new TypeError(`Invalid member content document: ${result.errors.join("; ")}`);
|
|
2135
|
+
return result.value;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// src/member-content.ts
|
|
2139
|
+
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
2140
|
+
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
2141
|
+
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
2142
|
+
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);
|
|
2143
|
+
function composeMemberContent(parent, chapter) {
|
|
2144
|
+
const pages = [];
|
|
2145
|
+
const parentPages = /* @__PURE__ */ new Map();
|
|
2146
|
+
for (const page of parent?.pages ?? []) {
|
|
2147
|
+
const effective = { ...page, source: parent.source };
|
|
2148
|
+
parentPages.set(page.id, effective);
|
|
2149
|
+
pages.push(effective);
|
|
2150
|
+
}
|
|
2151
|
+
for (const override of chapter?.upsellOverrides ?? []) {
|
|
2152
|
+
const page = parentPages.get(override.pageId);
|
|
2153
|
+
if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
|
|
2154
|
+
}
|
|
2155
|
+
for (const page of chapter?.pages ?? []) {
|
|
2156
|
+
if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
|
|
2157
|
+
throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
|
|
2158
|
+
}
|
|
2159
|
+
pages.push({ ...page, source: chapter.source });
|
|
2160
|
+
}
|
|
2161
|
+
return {
|
|
2162
|
+
pages: pages.sort(pageOrder),
|
|
2163
|
+
parentRevision: parent?.revision ?? null,
|
|
2164
|
+
chapterRevision: chapter?.revision ?? null
|
|
1954
2165
|
};
|
|
2166
|
+
}
|
|
2167
|
+
var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
2168
|
+
function projectMemberContent(content, viewer, tiers2) {
|
|
2169
|
+
const authority = new Map(tiers2.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
1955
2170
|
return {
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
1978
|
-
],
|
|
1979
|
-
runbooks
|
|
2171
|
+
viewer: { ...viewer },
|
|
2172
|
+
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
2173
|
+
pages: content.pages.map((page) => {
|
|
2174
|
+
const canRead = allowed(page, viewer);
|
|
2175
|
+
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
2176
|
+
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
2177
|
+
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
2178
|
+
return {
|
|
2179
|
+
id: page.id,
|
|
2180
|
+
slug: page.slug,
|
|
2181
|
+
title: page.title,
|
|
2182
|
+
...page.summary ? { summary: page.summary } : {},
|
|
2183
|
+
navigation: page.navigation,
|
|
2184
|
+
audience: page.audience,
|
|
2185
|
+
source: page.source,
|
|
2186
|
+
access: canRead ? "allowed" : "locked",
|
|
2187
|
+
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
2188
|
+
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
2189
|
+
...upsell ? { upsell } : {}
|
|
2190
|
+
};
|
|
2191
|
+
})
|
|
1980
2192
|
};
|
|
1981
2193
|
}
|
|
1982
2194
|
|
|
2195
|
+
// src/member-content-canonical.ts
|
|
2196
|
+
var encoder = new TextEncoder();
|
|
2197
|
+
function canonicalValue(value) {
|
|
2198
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2199
|
+
return JSON.stringify(value);
|
|
2200
|
+
}
|
|
2201
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2202
|
+
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2203
|
+
const record3 = value;
|
|
2204
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2205
|
+
const item = record3[key];
|
|
2206
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2207
|
+
});
|
|
2208
|
+
return `{${entries.join(",")}}`;
|
|
2209
|
+
}
|
|
2210
|
+
function canonicalMemberContent(value) {
|
|
2211
|
+
return canonicalValue(value);
|
|
2212
|
+
}
|
|
2213
|
+
async function memberContentDigest(document) {
|
|
2214
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalMemberContent(document)));
|
|
2215
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2216
|
+
}
|
|
2217
|
+
async function createMemberContentEnvelope(document) {
|
|
2218
|
+
return { document, digest: await memberContentDigest(document) };
|
|
2219
|
+
}
|
|
2220
|
+
|
|
1983
2221
|
// src/email.ts
|
|
1984
2222
|
function render(template, vars) {
|
|
1985
2223
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -2153,6 +2391,213 @@ function canceledPatch() {
|
|
|
2153
2391
|
return { canceled: true };
|
|
2154
2392
|
}
|
|
2155
2393
|
|
|
2394
|
+
// src/admission-grants-contract.ts
|
|
2395
|
+
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
2396
|
+
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
2397
|
+
|
|
2398
|
+
// src/signup-control-canonical.ts
|
|
2399
|
+
var encoder2 = new TextEncoder();
|
|
2400
|
+
function canonicalValue2(value) {
|
|
2401
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2402
|
+
return JSON.stringify(value);
|
|
2403
|
+
}
|
|
2404
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2405
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2406
|
+
const record3 = value;
|
|
2407
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2408
|
+
const item = record3[key];
|
|
2409
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2410
|
+
});
|
|
2411
|
+
return `{${entries.join(",")}}`;
|
|
2412
|
+
}
|
|
2413
|
+
function canonicalSignupControl(value) {
|
|
2414
|
+
return canonicalValue2(value);
|
|
2415
|
+
}
|
|
2416
|
+
async function signupControlDigest(document) {
|
|
2417
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalSignupControl(document)));
|
|
2418
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2419
|
+
}
|
|
2420
|
+
async function createSignupControlEnvelope(document) {
|
|
2421
|
+
return { document, digest: await signupControlDigest(document) };
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// src/admission-grants.ts
|
|
2425
|
+
var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
2426
|
+
var MAX_GRANTS = 2e3;
|
|
2427
|
+
var MAX_NOTE = 2e3;
|
|
2428
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2429
|
+
var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
|
|
2430
|
+
function normalizeAdmissionEmail(email) {
|
|
2431
|
+
return normalizeChapterEmail(email);
|
|
2432
|
+
}
|
|
2433
|
+
function validateAdmissionGrantDocument(input) {
|
|
2434
|
+
const errors = [];
|
|
2435
|
+
const root = input && typeof input === "object" ? input : {};
|
|
2436
|
+
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2437
|
+
const revision = Number(root.revision);
|
|
2438
|
+
const generatedAt = Number(root.generatedAt);
|
|
2439
|
+
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
2440
|
+
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
2441
|
+
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
2442
|
+
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
2443
|
+
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
2444
|
+
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
2445
|
+
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
2446
|
+
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
2447
|
+
if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
2448
|
+
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
2449
|
+
if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
2450
|
+
if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
2451
|
+
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
2452
|
+
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
2453
|
+
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
2454
|
+
const grants = [];
|
|
2455
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2456
|
+
const seenEmails = /* @__PURE__ */ new Set();
|
|
2457
|
+
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
2458
|
+
const grant = raw && typeof raw === "object" ? raw : {};
|
|
2459
|
+
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
2460
|
+
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
2461
|
+
const normalized = normalizeAdmissionEmail(email);
|
|
2462
|
+
const decision = grant.decision;
|
|
2463
|
+
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
2464
|
+
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
2465
|
+
if (!ID2.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
2466
|
+
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
2467
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
2468
|
+
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
2469
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
2470
|
+
errors.push(`document.grants[${index}].decision is invalid`);
|
|
2471
|
+
}
|
|
2472
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
2473
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
2474
|
+
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
2475
|
+
}
|
|
2476
|
+
seenIds.add(id4);
|
|
2477
|
+
seenEmails.add(normalized);
|
|
2478
|
+
grants.push({
|
|
2479
|
+
id: id4,
|
|
2480
|
+
email,
|
|
2481
|
+
decision,
|
|
2482
|
+
...note ? { note } : {},
|
|
2483
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
if (errors.length) return { ok: false, errors };
|
|
2487
|
+
return {
|
|
2488
|
+
ok: true,
|
|
2489
|
+
value: {
|
|
2490
|
+
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
2491
|
+
revision,
|
|
2492
|
+
generatedAt,
|
|
2493
|
+
source: { appId: sourceId, actor: sourceActor },
|
|
2494
|
+
target: { chapterId, environment },
|
|
2495
|
+
grants
|
|
2496
|
+
}
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
async function createAdmissionGrantEnvelope(document) {
|
|
2500
|
+
const checked = validateAdmissionGrantDocument(document);
|
|
2501
|
+
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
2502
|
+
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
2503
|
+
}
|
|
2504
|
+
var rows = (value) => Array.isArray(value) ? value : [];
|
|
2505
|
+
function asGrant(row) {
|
|
2506
|
+
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
2507
|
+
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
2508
|
+
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
2509
|
+
return row;
|
|
2510
|
+
}
|
|
2511
|
+
async function findAdmissionGrant(db, targetId, email, now) {
|
|
2512
|
+
const emailNormalized = normalizeAdmissionEmail(email);
|
|
2513
|
+
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
2514
|
+
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
2515
|
+
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
2516
|
+
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
2517
|
+
)).sort((a, b) => {
|
|
2518
|
+
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
2519
|
+
return scope || b.createdAt - a.createdAt;
|
|
2520
|
+
});
|
|
2521
|
+
return active[0] ?? null;
|
|
2522
|
+
}
|
|
2523
|
+
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
2524
|
+
const where = { sourceId, targetId, environment };
|
|
2525
|
+
if (db.aggregate) {
|
|
2526
|
+
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
2527
|
+
}
|
|
2528
|
+
const found = await db.query({ admissionGrantRevisions: {} });
|
|
2529
|
+
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
2530
|
+
}
|
|
2531
|
+
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
2532
|
+
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
2533
|
+
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
2534
|
+
where: { sourceId, targetId, environment },
|
|
2535
|
+
order: { revision: "desc" },
|
|
2536
|
+
limit: 1
|
|
2537
|
+
} } });
|
|
2538
|
+
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
2539
|
+
return row ? row : null;
|
|
2540
|
+
}
|
|
2541
|
+
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
2542
|
+
const { document, digest } = envelope;
|
|
2543
|
+
const current = rows((await db.query({ admissionGrants: { $: {
|
|
2544
|
+
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
2545
|
+
limit: MAX_GRANTS
|
|
2546
|
+
} } })).admissionGrants);
|
|
2547
|
+
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
2548
|
+
const ops = [];
|
|
2549
|
+
for (const row of current) {
|
|
2550
|
+
const id4 = String(row.id ?? "");
|
|
2551
|
+
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
2552
|
+
active: false,
|
|
2553
|
+
revokedAt: receivedAt,
|
|
2554
|
+
revokedBy: document.source.actor,
|
|
2555
|
+
updatedAt: receivedAt,
|
|
2556
|
+
updatedBy: document.source.actor
|
|
2557
|
+
} });
|
|
2558
|
+
}
|
|
2559
|
+
for (const grant of document.grants) {
|
|
2560
|
+
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
2561
|
+
const existing = current.find((row) => row.id === id4);
|
|
2562
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
2563
|
+
id: id4,
|
|
2564
|
+
sourceGrantId: grant.id,
|
|
2565
|
+
sourceScope: "parent",
|
|
2566
|
+
sourceId: document.source.appId,
|
|
2567
|
+
targetId: document.target.chapterId,
|
|
2568
|
+
email: grant.email,
|
|
2569
|
+
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
2570
|
+
decision: grant.decision,
|
|
2571
|
+
...grant.note ? { note: grant.note } : {},
|
|
2572
|
+
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
2573
|
+
active: true,
|
|
2574
|
+
sourceRevision: document.revision,
|
|
2575
|
+
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
2576
|
+
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
2577
|
+
updatedAt: receivedAt,
|
|
2578
|
+
updatedBy: document.source.actor
|
|
2579
|
+
} });
|
|
2580
|
+
const retract = [
|
|
2581
|
+
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
2582
|
+
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
2583
|
+
];
|
|
2584
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
2585
|
+
}
|
|
2586
|
+
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
2587
|
+
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
2588
|
+
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
2589
|
+
id: revisionId,
|
|
2590
|
+
sourceId: document.source.appId,
|
|
2591
|
+
targetId: document.target.chapterId,
|
|
2592
|
+
environment: document.target.environment,
|
|
2593
|
+
revision: document.revision,
|
|
2594
|
+
digest,
|
|
2595
|
+
active: true,
|
|
2596
|
+
receivedAt
|
|
2597
|
+
} });
|
|
2598
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2156
2601
|
// src/join-continuation.ts
|
|
2157
2602
|
function resolveJoinContinuation(input) {
|
|
2158
2603
|
if (input.status === "approved") return "accepted";
|
|
@@ -2174,10 +2619,10 @@ function resolveJoinContinuation(input) {
|
|
|
2174
2619
|
// src/gift-offers.ts
|
|
2175
2620
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2176
2621
|
var MAX_GIFT_OFFERS = 20;
|
|
2177
|
-
var
|
|
2622
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2178
2623
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2179
2624
|
var CURRENCY = /^[a-z]{3}$/;
|
|
2180
|
-
var
|
|
2625
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2181
2626
|
var text = (value, path, errors, max) => {
|
|
2182
2627
|
if (typeof value !== "string" || !value.trim()) {
|
|
2183
2628
|
errors.push(`${path} is required`);
|
|
@@ -2188,7 +2633,7 @@ var text = (value, path, errors, max) => {
|
|
|
2188
2633
|
};
|
|
2189
2634
|
function validateGiftOfferCatalog(input, tierIds) {
|
|
2190
2635
|
const errors = [];
|
|
2191
|
-
const root =
|
|
2636
|
+
const root = record2(input);
|
|
2192
2637
|
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2193
2638
|
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2194
2639
|
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
@@ -2199,18 +2644,18 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2199
2644
|
const offers = [];
|
|
2200
2645
|
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2201
2646
|
const path = `document.giftOffers.offers[${index}]`;
|
|
2202
|
-
const item =
|
|
2647
|
+
const item = record2(raw);
|
|
2203
2648
|
if (!item) {
|
|
2204
2649
|
errors.push(`${path} must be an object`);
|
|
2205
2650
|
continue;
|
|
2206
2651
|
}
|
|
2207
|
-
const
|
|
2208
|
-
for (const key of Object.keys(item)) if (!
|
|
2652
|
+
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2653
|
+
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2209
2654
|
const id4 = text(item.id, `${path}.id`, errors, 80);
|
|
2210
|
-
if (id4 && !
|
|
2655
|
+
if (id4 && !ID3.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2211
2656
|
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2212
2657
|
ids.add(id4);
|
|
2213
|
-
const targetRaw =
|
|
2658
|
+
const targetRaw = record2(item.target);
|
|
2214
2659
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2215
2660
|
if (targetRaw?.type === "tier") {
|
|
2216
2661
|
const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
@@ -2218,7 +2663,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2218
2663
|
target = { type: "tier", tierId };
|
|
2219
2664
|
} else if (targetRaw?.type === "entitlement") {
|
|
2220
2665
|
const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
2221
|
-
if (entitlementKey && !
|
|
2666
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2222
2667
|
target = { type: "entitlement", entitlementKey };
|
|
2223
2668
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2224
2669
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
@@ -2303,34 +2748,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
2303
2748
|
if (readable && raw.length <= 64) return raw;
|
|
2304
2749
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
2305
2750
|
}
|
|
2306
|
-
function normalizeSharedRecord(
|
|
2307
|
-
if ("input" in
|
|
2751
|
+
function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
2752
|
+
if ("input" in record3 && record3.version === 2) {
|
|
2308
2753
|
return {
|
|
2309
2754
|
version: 2,
|
|
2310
|
-
sourceId:
|
|
2311
|
-
sourceRecordId:
|
|
2312
|
-
type:
|
|
2313
|
-
input:
|
|
2755
|
+
sourceId: record3.source.siteId,
|
|
2756
|
+
sourceRecordId: record3.source.recordId,
|
|
2757
|
+
type: record3.type,
|
|
2758
|
+
input: record3.input
|
|
2314
2759
|
};
|
|
2315
2760
|
}
|
|
2316
|
-
if ("input" in
|
|
2761
|
+
if ("input" in record3) {
|
|
2317
2762
|
return {
|
|
2318
2763
|
version: 1,
|
|
2319
2764
|
sourceId: fallbackSourceId,
|
|
2320
|
-
sourceRecordId:
|
|
2321
|
-
type:
|
|
2322
|
-
input:
|
|
2765
|
+
sourceRecordId: record3.hubRecordId,
|
|
2766
|
+
type: record3.type,
|
|
2767
|
+
input: record3.input
|
|
2323
2768
|
};
|
|
2324
2769
|
}
|
|
2325
|
-
if ("type" in
|
|
2326
|
-
const input = { name:
|
|
2770
|
+
if ("type" in record3 && record3.type === "company") {
|
|
2771
|
+
const input = { name: record3.name };
|
|
2327
2772
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
2328
|
-
if (
|
|
2773
|
+
if (record3[key]) input[key] = record3[key];
|
|
2329
2774
|
}
|
|
2330
2775
|
return {
|
|
2331
2776
|
version: 1,
|
|
2332
2777
|
sourceId: fallbackSourceId,
|
|
2333
|
-
sourceRecordId:
|
|
2778
|
+
sourceRecordId: record3.hubRecordId,
|
|
2334
2779
|
type: "company",
|
|
2335
2780
|
input
|
|
2336
2781
|
};
|
|
@@ -2338,28 +2783,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
|
|
|
2338
2783
|
return {
|
|
2339
2784
|
version: 1,
|
|
2340
2785
|
sourceId: fallbackSourceId,
|
|
2341
|
-
sourceRecordId:
|
|
2786
|
+
sourceRecordId: record3.hubRecordId,
|
|
2342
2787
|
type: "person",
|
|
2343
|
-
input: sharedPersonInput(
|
|
2788
|
+
input: sharedPersonInput(record3)
|
|
2344
2789
|
};
|
|
2345
2790
|
}
|
|
2346
|
-
function sharedRecordFromCrm(crm,
|
|
2347
|
-
if (target.fields && !target.fields[
|
|
2348
|
-
throw new Error(`${target.name} does not accept "${
|
|
2791
|
+
function sharedRecordFromCrm(crm, record3, target, sourceId) {
|
|
2792
|
+
if (target.fields && !target.fields[record3.type]) {
|
|
2793
|
+
throw new Error(`${target.name} does not accept "${record3.type}" records`);
|
|
2349
2794
|
}
|
|
2350
|
-
const def = crm.type(
|
|
2351
|
-
const fields = target.fields?.[
|
|
2795
|
+
const def = crm.type(record3.type);
|
|
2796
|
+
const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
|
|
2352
2797
|
if (!fields) {
|
|
2353
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
2798
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
|
|
2354
2799
|
}
|
|
2355
2800
|
const nameField = def.nameField ?? "name";
|
|
2356
2801
|
const input = {};
|
|
2357
2802
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
2358
|
-
const value =
|
|
2803
|
+
const value = record3.fields?.[field];
|
|
2359
2804
|
if (value !== void 0) input[field] = value;
|
|
2360
2805
|
}
|
|
2361
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
2362
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
2806
|
+
if (input[nameField] === void 0) input[nameField] = record3.name;
|
|
2807
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
|
|
2363
2808
|
}
|
|
2364
2809
|
async function upsertPerson(deps, opts) {
|
|
2365
2810
|
const email = opts.email.toLowerCase();
|
|
@@ -2373,11 +2818,11 @@ async function upsertPerson(deps, opts) {
|
|
|
2373
2818
|
const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
2374
2819
|
return { recordId: created.id };
|
|
2375
2820
|
}
|
|
2376
|
-
async function findSharedRecord(deps,
|
|
2821
|
+
async function findSharedRecord(deps, record3, tag) {
|
|
2377
2822
|
const structured = await getRecordByOrigin(
|
|
2378
2823
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
2379
|
-
|
|
2380
|
-
|
|
2824
|
+
record3.sourceId,
|
|
2825
|
+
record3.sourceRecordId
|
|
2381
2826
|
);
|
|
2382
2827
|
if (structured) return structured;
|
|
2383
2828
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -2386,53 +2831,53 @@ async function findSharedRecord(deps, record2, tag) {
|
|
|
2386
2831
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
2387
2832
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2388
2833
|
}
|
|
2389
|
-
const def = deps.crm.type(
|
|
2834
|
+
const def = deps.crm.type(record3.type);
|
|
2390
2835
|
const emailField = def.emailField;
|
|
2391
|
-
if (emailField && typeof
|
|
2392
|
-
const primaryEmail =
|
|
2836
|
+
if (emailField && typeof record3.input[emailField] === "string") {
|
|
2837
|
+
const primaryEmail = record3.input[emailField].toLowerCase();
|
|
2393
2838
|
const found = await deps.db.query({
|
|
2394
|
-
crm_record: { $: { where: { type:
|
|
2839
|
+
crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
|
|
2395
2840
|
});
|
|
2396
2841
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2397
2842
|
}
|
|
2398
|
-
const domain =
|
|
2843
|
+
const domain = record3.input.domain;
|
|
2399
2844
|
const domainSlot = def.fields.domain?.slot;
|
|
2400
2845
|
if (typeof domain === "string" && domainSlot) {
|
|
2401
2846
|
const found = await deps.db.query({
|
|
2402
|
-
crm_record: { $: { where: { type:
|
|
2847
|
+
crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
|
|
2403
2848
|
});
|
|
2404
2849
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2405
2850
|
}
|
|
2406
2851
|
const nameField = def.nameField ?? "name";
|
|
2407
|
-
const name =
|
|
2408
|
-
if (
|
|
2852
|
+
const name = record3.input[nameField];
|
|
2853
|
+
if (record3.type === "company" && typeof name === "string" && name.trim()) {
|
|
2409
2854
|
const found = await deps.db.query({
|
|
2410
|
-
crm_record: { $: { where: { type:
|
|
2855
|
+
crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
|
|
2411
2856
|
});
|
|
2412
2857
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2413
2858
|
}
|
|
2414
2859
|
return void 0;
|
|
2415
2860
|
}
|
|
2416
2861
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
2417
|
-
const
|
|
2418
|
-
if (!
|
|
2862
|
+
const record3 = normalizeSharedRecord(shared, options.sourceId);
|
|
2863
|
+
if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
|
|
2419
2864
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
2420
2865
|
}
|
|
2421
|
-
if (options.sourceId && options.sourceId !==
|
|
2866
|
+
if (options.sourceId && options.sourceId !== record3.sourceId) {
|
|
2422
2867
|
throw new Error("signed sender does not match payload source");
|
|
2423
2868
|
}
|
|
2424
|
-
const tag = networkSourceTag(
|
|
2869
|
+
const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
|
|
2425
2870
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
2426
|
-
const existing = await findSharedRecord(deps,
|
|
2871
|
+
const existing = await findSharedRecord(deps, record3, tag);
|
|
2427
2872
|
let recordId;
|
|
2428
2873
|
if (existing && typeof existing.id === "string") {
|
|
2429
|
-
await updateRecord(crmDeps, { id: existing.id, input:
|
|
2874
|
+
await updateRecord(crmDeps, { id: existing.id, input: record3.input });
|
|
2430
2875
|
recordId = existing.id;
|
|
2431
2876
|
} else {
|
|
2432
|
-
recordId = `network_${shortHash(`${
|
|
2877
|
+
recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
|
|
2433
2878
|
await createRecord({ ...crmDeps, newId: () => recordId }, {
|
|
2434
|
-
type:
|
|
2435
|
-
input:
|
|
2879
|
+
type: record3.type,
|
|
2880
|
+
input: record3.input,
|
|
2436
2881
|
mutationId: `share-create:${tag}`
|
|
2437
2882
|
});
|
|
2438
2883
|
}
|
|
@@ -2442,10 +2887,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
2442
2887
|
);
|
|
2443
2888
|
await upsertRecordOrigin(crmDeps, {
|
|
2444
2889
|
recordId,
|
|
2445
|
-
sourceId:
|
|
2446
|
-
sourceRecordId:
|
|
2890
|
+
sourceId: record3.sourceId,
|
|
2891
|
+
sourceRecordId: record3.sourceRecordId,
|
|
2447
2892
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
2448
|
-
payloadVersion:
|
|
2893
|
+
payloadVersion: record3.version
|
|
2449
2894
|
});
|
|
2450
2895
|
return { recordId };
|
|
2451
2896
|
}
|
|
@@ -2729,8 +3174,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
2729
3174
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
2730
3175
|
const page = Array.isArray(data) ? data : [];
|
|
2731
3176
|
for (const u of page) {
|
|
2732
|
-
const
|
|
2733
|
-
if (
|
|
3177
|
+
const record3 = toRecord(u);
|
|
3178
|
+
if (record3) out.push(record3);
|
|
2734
3179
|
}
|
|
2735
3180
|
if (page.length < PAGE) break;
|
|
2736
3181
|
}
|
|
@@ -2832,7 +3277,8 @@ function applicationSummary(app) {
|
|
|
2832
3277
|
meetingLink: app.meetingLink ?? null,
|
|
2833
3278
|
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
2834
3279
|
renewalAt: app.renewalAt ?? null,
|
|
2835
|
-
canceled: app.canceled === true
|
|
3280
|
+
canceled: app.canceled === true,
|
|
3281
|
+
tierId: app.tierId ?? null
|
|
2836
3282
|
};
|
|
2837
3283
|
}
|
|
2838
3284
|
function memberApplication(app, meeting, defaultTimezone) {
|
|
@@ -3136,7 +3582,7 @@ var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
|
3136
3582
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
3137
3583
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3138
3584
|
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
3139
|
-
var
|
|
3585
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
3140
3586
|
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
3141
3587
|
"tierId",
|
|
3142
3588
|
"variant",
|
|
@@ -3155,12 +3601,12 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
|
3155
3601
|
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
3156
3602
|
var MAX_TIERS = 20;
|
|
3157
3603
|
var MAX_BENEFITS = 24;
|
|
3158
|
-
var
|
|
3604
|
+
var MAX_MARKDOWN2 = 65536;
|
|
3159
3605
|
var MAX_LABEL = 160;
|
|
3160
3606
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3161
|
-
function rejectUnknownKeys(value,
|
|
3607
|
+
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
3162
3608
|
for (const key of Object.keys(value)) {
|
|
3163
|
-
if (!
|
|
3609
|
+
if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3164
3610
|
}
|
|
3165
3611
|
}
|
|
3166
3612
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -3181,7 +3627,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3181
3627
|
const errors = [];
|
|
3182
3628
|
const root = objectRecord(input);
|
|
3183
3629
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3184
|
-
rejectUnknownKeys(root,
|
|
3630
|
+
rejectUnknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
3185
3631
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3186
3632
|
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
3187
3633
|
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
@@ -3235,7 +3681,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3235
3681
|
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
3236
3682
|
else {
|
|
3237
3683
|
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
3238
|
-
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors,
|
|
3684
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN2, true);
|
|
3239
3685
|
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
3240
3686
|
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
3241
3687
|
}
|
|
@@ -3257,10 +3703,10 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3257
3703
|
}
|
|
3258
3704
|
}
|
|
3259
3705
|
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
3260
|
-
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors,
|
|
3706
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN2);
|
|
3261
3707
|
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
3262
3708
|
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
3263
|
-
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors,
|
|
3709
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN2);
|
|
3264
3710
|
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
3265
3711
|
tiers2.push({
|
|
3266
3712
|
tierId,
|
|
@@ -3282,7 +3728,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3282
3728
|
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
3283
3729
|
}
|
|
3284
3730
|
}
|
|
3285
|
-
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors,
|
|
3731
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN2);
|
|
3286
3732
|
if (errors.length) return { ok: false, errors };
|
|
3287
3733
|
return {
|
|
3288
3734
|
ok: true,
|
|
@@ -3301,11 +3747,11 @@ function membershipPageDocument(input, options = {}) {
|
|
|
3301
3747
|
}
|
|
3302
3748
|
|
|
3303
3749
|
// src/signup-control-validate.ts
|
|
3304
|
-
var
|
|
3750
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3305
3751
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
3306
3752
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3307
3753
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
3308
|
-
var
|
|
3754
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set([
|
|
3309
3755
|
"schemaVersion",
|
|
3310
3756
|
"revision",
|
|
3311
3757
|
"generatedAt",
|
|
@@ -3321,8 +3767,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
3321
3767
|
]);
|
|
3322
3768
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
3323
3769
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3324
|
-
function
|
|
3325
|
-
for (const key of Object.keys(value)) if (!
|
|
3770
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
3771
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3326
3772
|
}
|
|
3327
3773
|
function text2(value, path, errors, max = 65536) {
|
|
3328
3774
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -3346,7 +3792,7 @@ function tiersOf(value, errors) {
|
|
|
3346
3792
|
errors.push(`${path} must be an object`);
|
|
3347
3793
|
return [];
|
|
3348
3794
|
}
|
|
3349
|
-
|
|
3795
|
+
unknownKeys2(item, TIER_KEYS2, path, errors);
|
|
3350
3796
|
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
3351
3797
|
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3352
3798
|
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
@@ -3382,7 +3828,7 @@ function applicationOf(value, errors) {
|
|
|
3382
3828
|
errors.push("document.application must be an object");
|
|
3383
3829
|
return void 0;
|
|
3384
3830
|
}
|
|
3385
|
-
|
|
3831
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
3386
3832
|
try {
|
|
3387
3833
|
const resolved = resolveApplication(input);
|
|
3388
3834
|
const copy = recordOf(input.copy);
|
|
@@ -3407,10 +3853,10 @@ function policyOf(value, errors) {
|
|
|
3407
3853
|
errors.push("document.policy must be an object");
|
|
3408
3854
|
return void 0;
|
|
3409
3855
|
}
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3856
|
+
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
3857
|
+
unknownKeys2(input, allowed2, "document.policy", errors);
|
|
3412
3858
|
const result = {};
|
|
3413
|
-
for (const key of
|
|
3859
|
+
for (const key of allowed2) {
|
|
3414
3860
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3415
3861
|
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
3416
3862
|
}
|
|
@@ -3424,7 +3870,7 @@ function themeOf(value, errors) {
|
|
|
3424
3870
|
errors.push("document.theme.tokens must be an object");
|
|
3425
3871
|
return void 0;
|
|
3426
3872
|
}
|
|
3427
|
-
|
|
3873
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
3428
3874
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
3429
3875
|
let serializedLength = 0;
|
|
3430
3876
|
for (const [key, token] of Object.entries(tokens)) {
|
|
@@ -3444,7 +3890,7 @@ function validateSignupControlDocument(input) {
|
|
|
3444
3890
|
const errors = [];
|
|
3445
3891
|
const root = recordOf(input);
|
|
3446
3892
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3447
|
-
|
|
3893
|
+
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
3448
3894
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3449
3895
|
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
3450
3896
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
@@ -3453,8 +3899,8 @@ function validateSignupControlDocument(input) {
|
|
|
3453
3899
|
const appId = text2(source?.appId, "document.source.appId", errors, 63);
|
|
3454
3900
|
const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
3455
3901
|
const environment = text2(target?.environment, "document.target.environment", errors, 63);
|
|
3456
|
-
if (appId && !
|
|
3457
|
-
if (chapterId && !
|
|
3902
|
+
if (appId && !SLUG6.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3903
|
+
if (chapterId && !SLUG6.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3458
3904
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
3459
3905
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
3460
3906
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
@@ -3604,7 +4050,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
|
3604
4050
|
|
|
3605
4051
|
// src/membership-markdown.ts
|
|
3606
4052
|
var MAX_BENEFITS2 = 24;
|
|
3607
|
-
var
|
|
4053
|
+
var MAX_MARKDOWN3 = 65536;
|
|
3608
4054
|
var MAX_LABEL2 = 160;
|
|
3609
4055
|
var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
|
|
3610
4056
|
var MARKDOWN_HEADING = /^#{1,6}\s+/;
|
|
@@ -3638,8 +4084,8 @@ function markdownBenefits(lines, path, errors) {
|
|
|
3638
4084
|
}
|
|
3639
4085
|
function applyMembershipTierMarkdown(presentation, source) {
|
|
3640
4086
|
if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
|
|
3641
|
-
if (source.length >
|
|
3642
|
-
return { ok: false, errors: [`membership Markdown must be at most ${
|
|
4087
|
+
if (source.length > MAX_MARKDOWN3) {
|
|
4088
|
+
return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN3.toLocaleString("en-US")} characters`] };
|
|
3643
4089
|
}
|
|
3644
4090
|
if (!source.trim()) return { ok: true, value: { ...presentation } };
|
|
3645
4091
|
const errors = [];
|
|
@@ -3817,12 +4263,16 @@ async function exportChapterPromotion(input) {
|
|
|
3817
4263
|
export {
|
|
3818
4264
|
ADMISSION_GRANTS_PATH,
|
|
3819
4265
|
ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
4266
|
+
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
3820
4267
|
CHAPTER_PROMOTION_ADAPTER,
|
|
3821
4268
|
DEFAULT_CHAPTER_COPY,
|
|
3822
4269
|
DEFAULT_SHARE_FIELDS,
|
|
3823
4270
|
GIFT_OFFER_CATALOG_VERSION,
|
|
3824
4271
|
MAX_GIFT_OFFERS,
|
|
3825
4272
|
MEMBERSHIP_PAGE_RENDERER,
|
|
4273
|
+
MEMBER_CONTENT_CONTROL_PATH,
|
|
4274
|
+
MEMBER_CONTENT_PATH,
|
|
4275
|
+
MEMBER_CONTENT_SCHEMA_VERSION,
|
|
3826
4276
|
SCHEDULING_DEFAULTS,
|
|
3827
4277
|
SIGNUP_CONTROL_PATH,
|
|
3828
4278
|
SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
@@ -3830,6 +4280,7 @@ export {
|
|
|
3830
4280
|
SIGNUP_CONTROL_THEME_STYLE_ID,
|
|
3831
4281
|
applicantProfile,
|
|
3832
4282
|
applicationBookingUpdate,
|
|
4283
|
+
applicationFlowFields,
|
|
3833
4284
|
applicationSummary,
|
|
3834
4285
|
applyMembershipTierMarkdown,
|
|
3835
4286
|
backfillCrm,
|
|
@@ -3843,9 +4294,11 @@ export {
|
|
|
3843
4294
|
canChangeRole,
|
|
3844
4295
|
canTransition,
|
|
3845
4296
|
canceledPatch,
|
|
4297
|
+
canonicalMemberContent,
|
|
3846
4298
|
canonicalSignupControl,
|
|
3847
4299
|
chapterBrandFromTokens,
|
|
3848
4300
|
chapterDb,
|
|
4301
|
+
chapterFlowFieldContract,
|
|
3849
4302
|
clampArray,
|
|
3850
4303
|
clerkGetUser,
|
|
3851
4304
|
clerkGetUserByEmail,
|
|
@@ -3854,10 +4307,12 @@ export {
|
|
|
3854
4307
|
clerkListUsers,
|
|
3855
4308
|
clerkSetRole,
|
|
3856
4309
|
clerkUserRequest,
|
|
4310
|
+
composeMemberContent,
|
|
3857
4311
|
createAdmissionGrantEnvelope,
|
|
3858
4312
|
createChapterIntegration,
|
|
3859
4313
|
createClerkInvitation,
|
|
3860
4314
|
createClerkUser,
|
|
4315
|
+
createMemberContentEnvelope,
|
|
3861
4316
|
createSignupControlEnvelope,
|
|
3862
4317
|
dashboardMetricData,
|
|
3863
4318
|
defaultCrm,
|
|
@@ -3872,6 +4327,7 @@ export {
|
|
|
3872
4327
|
firstPaymentPatch,
|
|
3873
4328
|
formatChapterCopy,
|
|
3874
4329
|
formationFields,
|
|
4330
|
+
formationFlowFields,
|
|
3875
4331
|
getVaultSecret,
|
|
3876
4332
|
giftOfferActive,
|
|
3877
4333
|
hasDisclaimerAck,
|
|
@@ -3893,11 +4349,14 @@ export {
|
|
|
3893
4349
|
meetingCreateRow,
|
|
3894
4350
|
meetingRescheduleUpdate,
|
|
3895
4351
|
memberApplication,
|
|
4352
|
+
memberContentDigest,
|
|
4353
|
+
memberContentDocument,
|
|
3896
4354
|
memberSession,
|
|
3897
4355
|
membershipPageDocument,
|
|
3898
4356
|
membershipTierPresentationFromMarkdown,
|
|
3899
4357
|
networkSourceTag,
|
|
3900
4358
|
normalizeAdmissionEmail,
|
|
4359
|
+
normalizeChapterEmail,
|
|
3901
4360
|
normalizeSharedRecord,
|
|
3902
4361
|
normalizeWebhookEvent,
|
|
3903
4362
|
offerableTiers,
|
|
@@ -3905,6 +4364,7 @@ export {
|
|
|
3905
4364
|
personInputFromApp,
|
|
3906
4365
|
planDelivery,
|
|
3907
4366
|
projectApplicant,
|
|
4367
|
+
projectMemberContent,
|
|
3908
4368
|
projectSharedRecord,
|
|
3909
4369
|
publicGiftOffers,
|
|
3910
4370
|
reconcileMeetings,
|
|
@@ -3919,6 +4379,7 @@ export {
|
|
|
3919
4379
|
resolveChapterCopy,
|
|
3920
4380
|
resolveJoinContinuation,
|
|
3921
4381
|
resolveLeaderFormation,
|
|
4382
|
+
resolveMemberContent,
|
|
3922
4383
|
resolvePipeline,
|
|
3923
4384
|
resolveScheduling,
|
|
3924
4385
|
resolveSignupControl,
|
|
@@ -3942,6 +4403,7 @@ export {
|
|
|
3942
4403
|
updateClerkUserMetadataByEmail,
|
|
3943
4404
|
validateAdmissionGrantDocument,
|
|
3944
4405
|
validateGiftOfferCatalog,
|
|
4406
|
+
validateMemberContentDocument,
|
|
3945
4407
|
validateMembershipPageDocument,
|
|
3946
4408
|
validateScheduling,
|
|
3947
4409
|
validateSignupControlDocument,
|