@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/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/admission-grants-contract.ts
19
- var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
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 EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
74
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
277
75
  function isValidEmail(value) {
278
- return typeof value === "string" && EMAIL_RE2.test(value);
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 allowed = (f) => app.profileFields.includes(f);
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) || !allowed(f)) continue;
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 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
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 = normalizeAdmissionEmail(fields.email);
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
- const def = crm.type(formation.type);
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" || !SLUG3.test(id4)) {
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
- id: "chapter",
1957
- title: `Chapter: ${chapter.name}`,
1958
- npm: "@odla-ai/chapter",
1959
- schema: {
1960
- entities: { ...crmDesc.schema.entities, ...chapter.schema.entities },
1961
- links: { ...crmDesc.schema.links, ...chapter.schema.links }
1962
- },
1963
- rules: { ...crmDesc.rules, ...chapter.rules },
1964
- seeds,
1965
- probes: [
1966
- ...crmDesc.probes ?? [],
1967
- // The snapshot route is deliberately private. A credential-free smoke
1968
- // request must prove that the route is mounted and closed.
1969
- { path: "/api/network/snapshot", expectedStatus: 401 },
1970
- ...chapter.network.readers.length > 0 ? [{ path: "/api/network/records", expectedStatus: 401 }] : [],
1971
- ...chapter.network.readers.some((reader) => reader.sharedNotes.length > 0) ? [{
1972
- path: "/api/network/shared-notes",
1973
- expectedStatus: 401
1974
- }] : [],
1975
- // Formation metadata is public only when the host explicitly enables
1976
- // the bounded intake contract.
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 ID2 = /^[a-z][a-z0-9-]{1,79}$/;
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 record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
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 = record(input);
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 = record(raw);
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 allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
2208
- for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
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 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
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 = record(item.target);
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 && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
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(record2, fallbackSourceId = "legacy-source") {
2307
- if ("input" in record2 && record2.version === 2) {
2751
+ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
2752
+ if ("input" in record3 && record3.version === 2) {
2308
2753
  return {
2309
2754
  version: 2,
2310
- sourceId: record2.source.siteId,
2311
- sourceRecordId: record2.source.recordId,
2312
- type: record2.type,
2313
- input: record2.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 record2) {
2761
+ if ("input" in record3) {
2317
2762
  return {
2318
2763
  version: 1,
2319
2764
  sourceId: fallbackSourceId,
2320
- sourceRecordId: record2.hubRecordId,
2321
- type: record2.type,
2322
- input: record2.input
2765
+ sourceRecordId: record3.hubRecordId,
2766
+ type: record3.type,
2767
+ input: record3.input
2323
2768
  };
2324
2769
  }
2325
- if ("type" in record2 && record2.type === "company") {
2326
- const input = { name: record2.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 (record2[key]) input[key] = record2[key];
2773
+ if (record3[key]) input[key] = record3[key];
2329
2774
  }
2330
2775
  return {
2331
2776
  version: 1,
2332
2777
  sourceId: fallbackSourceId,
2333
- sourceRecordId: record2.hubRecordId,
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: record2.hubRecordId,
2786
+ sourceRecordId: record3.hubRecordId,
2342
2787
  type: "person",
2343
- input: sharedPersonInput(record2)
2788
+ input: sharedPersonInput(record3)
2344
2789
  };
2345
2790
  }
2346
- function sharedRecordFromCrm(crm, record2, target, sourceId) {
2347
- if (target.fields && !target.fields[record2.type]) {
2348
- throw new Error(`${target.name} does not accept "${record2.type}" records`);
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(record2.type);
2351
- const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
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 "${record2.type}" records`);
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 = record2.fields?.[field];
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] = record2.name;
2362
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
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, record2, tag) {
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
- record2.sourceId,
2380
- record2.sourceRecordId
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(record2.type);
2834
+ const def = deps.crm.type(record3.type);
2390
2835
  const emailField = def.emailField;
2391
- if (emailField && typeof record2.input[emailField] === "string") {
2392
- const primaryEmail = record2.input[emailField].toLowerCase();
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: record2.type, primaryEmail }, limit: 1 } }
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 = record2.input.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: record2.type, [domainSlot]: domain }, limit: 1 } }
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 = record2.input[nameField];
2408
- if (record2.type === "company" && typeof name === "string" && name.trim()) {
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: record2.type, name: name.trim() }, limit: 1 } }
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 record2 = normalizeSharedRecord(shared, options.sourceId);
2418
- if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
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 !== record2.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(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
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, record2, tag);
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: record2.input });
2874
+ await updateRecord(crmDeps, { id: existing.id, input: record3.input });
2430
2875
  recordId = existing.id;
2431
2876
  } else {
2432
- recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
2877
+ recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
2433
2878
  await createRecord({ ...crmDeps, newId: () => recordId }, {
2434
- type: record2.type,
2435
- input: record2.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: record2.sourceId,
2446
- sourceRecordId: record2.sourceRecordId,
2890
+ sourceId: record3.sourceId,
2891
+ sourceRecordId: record3.sourceRecordId,
2447
2892
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
2448
- payloadVersion: record2.version
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 record2 = toRecord(u);
2733
- if (record2) out.push(record2);
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 ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
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 MAX_MARKDOWN = 65536;
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, allowed, path, errors) {
3607
+ function rejectUnknownKeys(value, allowed2, path, errors) {
3162
3608
  for (const key of Object.keys(value)) {
3163
- if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
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, ROOT_KEYS, "document", errors);
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, MAX_MARKDOWN, true);
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, MAX_MARKDOWN);
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, MAX_MARKDOWN);
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, MAX_MARKDOWN);
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 SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
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 ROOT_KEYS2 = /* @__PURE__ */ new Set([
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 unknownKeys(value, allowed, path, errors) {
3325
- for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
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
- unknownKeys(item, TIER_KEYS2, path, errors);
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
- unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
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 allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
3411
- unknownKeys(input, allowed, "document.policy", errors);
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 allowed) {
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
- unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
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
- unknownKeys(root, ROOT_KEYS2, "document", errors);
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 && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
3457
- if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
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 MAX_MARKDOWN2 = 65536;
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 > MAX_MARKDOWN2) {
3642
- return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN2.toLocaleString("en-US")} characters`] };
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,