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