@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.
@@ -23,6 +23,7 @@ __export(worker_exports, {
23
23
  chapterWorker: () => chapterWorker,
24
24
  createWorkerContext: () => createWorkerContext,
25
25
  deliverAdmissionGrants: () => deliverAdmissionGrants,
26
+ deliverMemberContent: () => deliverMemberContent,
26
27
  deliverSignupControl: () => deliverSignupControl,
27
28
  renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
28
29
  withActiveSignupControlTheme: () => withActiveSignupControlTheme
@@ -198,204 +199,10 @@ async function applicationIdForSubmission(submissionId) {
198
199
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
199
200
  }
200
201
 
201
- // src/admission-grants-contract.ts
202
- var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
203
- var ADMISSION_GRANTS_PATH = "/api/admission-grants";
204
-
205
- // src/signup-control-canonical.ts
206
- var encoder = new TextEncoder();
207
- function canonicalValue(value) {
208
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
209
- return JSON.stringify(value);
210
- }
211
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
212
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
213
- const record2 = value;
214
- const entries = Object.keys(record2).sort().flatMap((key) => {
215
- const item = record2[key];
216
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
217
- });
218
- return `{${entries.join(",")}}`;
219
- }
220
- function canonicalSignupControl(value) {
221
- return canonicalValue(value);
222
- }
223
- async function signupControlDigest(document) {
224
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
225
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
226
- }
227
-
228
- // src/admission-grants.ts
229
- var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
230
- var MAX_GRANTS = 2e3;
231
- var MAX_NOTE = 2e3;
232
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
233
- var isAdmissionEmail = (value) => EMAIL_RE.test(value);
234
- function normalizeAdmissionEmail(email) {
202
+ // src/email-identity.ts
203
+ function normalizeChapterEmail(email) {
235
204
  return email.trim().toLowerCase();
236
205
  }
237
- function validateAdmissionGrantDocument(input) {
238
- const errors = [];
239
- const root = input && typeof input === "object" ? input : {};
240
- if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
241
- const revision = Number(root.revision);
242
- const generatedAt = Number(root.generatedAt);
243
- if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
244
- if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
245
- const source = root.source && typeof root.source === "object" ? root.source : {};
246
- const target = root.target && typeof root.target === "object" ? root.target : {};
247
- const sourceId = typeof source.appId === "string" ? source.appId : "";
248
- const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
249
- const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
250
- const environment = typeof target.environment === "string" ? target.environment : "";
251
- if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
252
- if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
253
- if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
254
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
255
- if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
256
- const rawGrants = Array.isArray(root.grants) ? root.grants : [];
257
- if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
258
- const grants = [];
259
- const seenIds = /* @__PURE__ */ new Set();
260
- const seenEmails = /* @__PURE__ */ new Set();
261
- for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
262
- const grant = raw && typeof raw === "object" ? raw : {};
263
- const id = typeof grant.id === "string" ? grant.id : "";
264
- const email = typeof grant.email === "string" ? grant.email.trim() : "";
265
- const normalized = normalizeAdmissionEmail(email);
266
- const decision = grant.decision;
267
- const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
268
- const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
269
- if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
270
- if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
271
- if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
272
- if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
273
- if (decision !== "waive_interview" && decision !== "require_interview") {
274
- errors.push(`document.grants[${index}].decision is invalid`);
275
- }
276
- if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
277
- if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
278
- errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
279
- }
280
- seenIds.add(id);
281
- seenEmails.add(normalized);
282
- grants.push({
283
- id,
284
- email,
285
- decision,
286
- ...note ? { note } : {},
287
- ...expiresAt !== void 0 ? { expiresAt } : {}
288
- });
289
- }
290
- if (errors.length) return { ok: false, errors };
291
- return {
292
- ok: true,
293
- value: {
294
- schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
295
- revision,
296
- generatedAt,
297
- source: { appId: sourceId, actor: sourceActor },
298
- target: { chapterId, environment },
299
- grants
300
- }
301
- };
302
- }
303
- var rows = (value) => Array.isArray(value) ? value : [];
304
- function asGrant(row) {
305
- if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
306
- if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
307
- if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
308
- return row;
309
- }
310
- async function findAdmissionGrant(db, targetId, email, now) {
311
- const emailNormalized = normalizeAdmissionEmail(email);
312
- if (!isAdmissionEmail(emailNormalized)) return null;
313
- const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
314
- const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
315
- grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
316
- )).sort((a, b) => {
317
- const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
318
- return scope || b.createdAt - a.createdAt;
319
- });
320
- return active[0] ?? null;
321
- }
322
- async function hasAdmissionRevision(db, sourceId, targetId, environment) {
323
- const where = { sourceId, targetId, environment };
324
- if (db.aggregate) {
325
- return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
326
- }
327
- const found = await db.query({ admissionGrantRevisions: {} });
328
- return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
329
- }
330
- async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
331
- if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
332
- const result = await db.query({ admissionGrantRevisions: { $: {
333
- where: { sourceId, targetId, environment },
334
- order: { revision: "desc" },
335
- limit: 1
336
- } } });
337
- const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
338
- return row ? row : null;
339
- }
340
- async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
341
- const { document, digest } = envelope;
342
- const current = rows((await db.query({ admissionGrants: { $: {
343
- where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
344
- limit: MAX_GRANTS
345
- } } })).admissionGrants);
346
- const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
347
- const ops = [];
348
- for (const row of current) {
349
- const id = String(row.id ?? "");
350
- if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
351
- active: false,
352
- revokedAt: receivedAt,
353
- revokedBy: document.source.actor,
354
- updatedAt: receivedAt,
355
- updatedBy: document.source.actor
356
- } });
357
- }
358
- for (const grant of document.grants) {
359
- const id = `admission:parent:${document.source.appId}:${grant.id}`;
360
- const existing = current.find((row) => row.id === id);
361
- ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
362
- id,
363
- sourceGrantId: grant.id,
364
- sourceScope: "parent",
365
- sourceId: document.source.appId,
366
- targetId: document.target.chapterId,
367
- email: grant.email,
368
- emailNormalized: normalizeAdmissionEmail(grant.email),
369
- decision: grant.decision,
370
- ...grant.note ? { note: grant.note } : {},
371
- ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
372
- active: true,
373
- sourceRevision: document.revision,
374
- createdAt: Number(existing?.createdAt ?? receivedAt),
375
- createdBy: String(existing?.createdBy ?? document.source.actor),
376
- updatedAt: receivedAt,
377
- updatedBy: document.source.actor
378
- } });
379
- const retract = [
380
- ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
381
- ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
382
- ];
383
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
384
- }
385
- if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
386
- const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
387
- ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
388
- id: revisionId,
389
- sourceId: document.source.appId,
390
- targetId: document.target.chapterId,
391
- environment: document.target.environment,
392
- revision: document.revision,
393
- digest,
394
- active: true,
395
- receivedAt
396
- } });
397
- await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
398
- }
399
206
 
400
207
  // src/member.ts
401
208
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
@@ -448,9 +255,9 @@ function resolveApplication(a) {
448
255
  validateEmail: a?.validateEmail ?? true
449
256
  };
450
257
  }
451
- var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
258
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
452
259
  function isValidEmail(value) {
453
- return typeof value === "string" && EMAIL_RE2.test(value);
260
+ return typeof value === "string" && EMAIL_RE.test(value);
454
261
  }
455
262
  function clampArray(value, max) {
456
263
  if (!Array.isArray(value)) return value;
@@ -462,14 +269,14 @@ function hasDisclaimerAck(fields) {
462
269
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
463
270
  function applicantProfile(chapter, fields) {
464
271
  const app = chapter.application;
465
- const allowed = (f) => app.profileFields.includes(f);
272
+ const allowed2 = (f) => app.profileFields.includes(f);
466
273
  const profile = {};
467
274
  for (const f of [...app.required, ...app.optional]) {
468
- if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
275
+ if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
469
276
  const v = fields[f];
470
277
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
471
278
  }
472
- if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
279
+ if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
473
280
  return Object.keys(profile).length > 0 ? profile : void 0;
474
281
  }
475
282
  async function submitApplication(db, chapter, fields, opts) {
@@ -485,7 +292,7 @@ async function submitApplication(db, chapter, fields, opts) {
485
292
  const cap = app.maxLen[f] ?? app.defaultMaxLen;
486
293
  if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
487
294
  }
488
- if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
295
+ if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
489
296
  return { ok: false, error: "email must be a valid email address" };
490
297
  }
491
298
  const acked = hasDisclaimerAck(fields);
@@ -501,7 +308,7 @@ async function submitApplication(db, chapter, fields, opts) {
501
308
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
502
309
  if (opts.groupId) row.groupId = opts.groupId;
503
310
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
504
- row.emailNormalized = normalizeAdmissionEmail(fields.email);
311
+ row.emailNormalized = normalizeChapterEmail(fields.email);
505
312
  }
506
313
  if (opts.admissionGrant?.decision === "waive_interview") {
507
314
  row.admissionGrantId = opts.admissionGrant.id;
@@ -718,14 +525,289 @@ function meetingCreateRow(i) {
718
525
  createdAt: i.createdAt
719
526
  };
720
527
  }
721
- function meetingRescheduleUpdate(startAt, endAt) {
722
- return { startAt, endAt, drift: "none" };
528
+ function meetingRescheduleUpdate(startAt, endAt) {
529
+ return { startAt, endAt, drift: "none" };
530
+ }
531
+ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
532
+ return {
533
+ meetingAt: startAt,
534
+ ...htmlLink ? { meetingLink: htmlLink } : {},
535
+ ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
536
+ };
537
+ }
538
+
539
+ // src/admission-grants-contract.ts
540
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
541
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
542
+
543
+ // src/signup-control-canonical.ts
544
+ var encoder = new TextEncoder();
545
+ function canonicalValue(value) {
546
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
547
+ return JSON.stringify(value);
548
+ }
549
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
550
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
551
+ const record3 = value;
552
+ const entries = Object.keys(record3).sort().flatMap((key) => {
553
+ const item = record3[key];
554
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
555
+ });
556
+ return `{${entries.join(",")}}`;
557
+ }
558
+ function canonicalSignupControl(value) {
559
+ return canonicalValue(value);
560
+ }
561
+ async function signupControlDigest(document) {
562
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
563
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
564
+ }
565
+
566
+ // src/admission-grants.ts
567
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
568
+ var MAX_GRANTS = 2e3;
569
+ var MAX_NOTE = 2e3;
570
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
571
+ var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
572
+ function normalizeAdmissionEmail(email) {
573
+ return normalizeChapterEmail(email);
574
+ }
575
+ function validateAdmissionGrantDocument(input) {
576
+ const errors = [];
577
+ const root = input && typeof input === "object" ? input : {};
578
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
579
+ const revision = Number(root.revision);
580
+ const generatedAt = Number(root.generatedAt);
581
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
582
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
583
+ const source = root.source && typeof root.source === "object" ? root.source : {};
584
+ const target = root.target && typeof root.target === "object" ? root.target : {};
585
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
586
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
587
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
588
+ const environment = typeof target.environment === "string" ? target.environment : "";
589
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
590
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
591
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
592
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
593
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
594
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
595
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
596
+ const grants = [];
597
+ const seenIds = /* @__PURE__ */ new Set();
598
+ const seenEmails = /* @__PURE__ */ new Set();
599
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
600
+ const grant = raw && typeof raw === "object" ? raw : {};
601
+ const id = typeof grant.id === "string" ? grant.id : "";
602
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
603
+ const normalized = normalizeAdmissionEmail(email);
604
+ const decision = grant.decision;
605
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
606
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
607
+ if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
608
+ if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
609
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
610
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
611
+ if (decision !== "waive_interview" && decision !== "require_interview") {
612
+ errors.push(`document.grants[${index}].decision is invalid`);
613
+ }
614
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
615
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
616
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
617
+ }
618
+ seenIds.add(id);
619
+ seenEmails.add(normalized);
620
+ grants.push({
621
+ id,
622
+ email,
623
+ decision,
624
+ ...note ? { note } : {},
625
+ ...expiresAt !== void 0 ? { expiresAt } : {}
626
+ });
627
+ }
628
+ if (errors.length) return { ok: false, errors };
629
+ return {
630
+ ok: true,
631
+ value: {
632
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
633
+ revision,
634
+ generatedAt,
635
+ source: { appId: sourceId, actor: sourceActor },
636
+ target: { chapterId, environment },
637
+ grants
638
+ }
639
+ };
640
+ }
641
+ var rows = (value) => Array.isArray(value) ? value : [];
642
+ function asGrant(row) {
643
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
644
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
645
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
646
+ return row;
647
+ }
648
+ async function findAdmissionGrant(db, targetId, email, now) {
649
+ const emailNormalized = normalizeAdmissionEmail(email);
650
+ if (!isAdmissionEmail(emailNormalized)) return null;
651
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
652
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
653
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
654
+ )).sort((a, b) => {
655
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
656
+ return scope || b.createdAt - a.createdAt;
657
+ });
658
+ return active[0] ?? null;
659
+ }
660
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
661
+ const where = { sourceId, targetId, environment };
662
+ if (db.aggregate) {
663
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
664
+ }
665
+ const found = await db.query({ admissionGrantRevisions: {} });
666
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
667
+ }
668
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
669
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
670
+ const result = await db.query({ admissionGrantRevisions: { $: {
671
+ where: { sourceId, targetId, environment },
672
+ order: { revision: "desc" },
673
+ limit: 1
674
+ } } });
675
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
676
+ return row ? row : null;
677
+ }
678
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
679
+ const { document, digest } = envelope;
680
+ const current = rows((await db.query({ admissionGrants: { $: {
681
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
682
+ limit: MAX_GRANTS
683
+ } } })).admissionGrants);
684
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
685
+ const ops = [];
686
+ for (const row of current) {
687
+ const id = String(row.id ?? "");
688
+ if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
689
+ active: false,
690
+ revokedAt: receivedAt,
691
+ revokedBy: document.source.actor,
692
+ updatedAt: receivedAt,
693
+ updatedBy: document.source.actor
694
+ } });
695
+ }
696
+ for (const grant of document.grants) {
697
+ const id = `admission:parent:${document.source.appId}:${grant.id}`;
698
+ const existing = current.find((row) => row.id === id);
699
+ ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
700
+ id,
701
+ sourceGrantId: grant.id,
702
+ sourceScope: "parent",
703
+ sourceId: document.source.appId,
704
+ targetId: document.target.chapterId,
705
+ email: grant.email,
706
+ emailNormalized: normalizeAdmissionEmail(grant.email),
707
+ decision: grant.decision,
708
+ ...grant.note ? { note: grant.note } : {},
709
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
710
+ active: true,
711
+ sourceRevision: document.revision,
712
+ createdAt: Number(existing?.createdAt ?? receivedAt),
713
+ createdBy: String(existing?.createdBy ?? document.source.actor),
714
+ updatedAt: receivedAt,
715
+ updatedBy: document.source.actor
716
+ } });
717
+ const retract = [
718
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
719
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
720
+ ];
721
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
722
+ }
723
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
724
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
725
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
726
+ id: revisionId,
727
+ sourceId: document.source.appId,
728
+ targetId: document.target.chapterId,
729
+ environment: document.target.environment,
730
+ revision: document.revision,
731
+ digest,
732
+ active: true,
733
+ receivedAt
734
+ } });
735
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
736
+ }
737
+
738
+ // src/flow-fields.ts
739
+ var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
740
+ var APPLICATION_LABELS = {
741
+ firstName: "First name",
742
+ lastName: "Last name",
743
+ email: "Email",
744
+ referral: "Referral source",
745
+ referralName: "Referral name",
746
+ whoYouAre: "Who you are",
747
+ linkedin: "LinkedIn",
748
+ message: "Intro message",
749
+ phone: "Phone",
750
+ state: "State"
751
+ };
752
+ function humanize(id) {
753
+ const text3 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
754
+ return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id;
755
+ }
756
+ function applicationDefs(chapter) {
757
+ try {
758
+ return chapter.crm.type("person").fields;
759
+ } catch {
760
+ return {};
761
+ }
762
+ }
763
+ function applicationType(id, def) {
764
+ if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
765
+ return id === "email" ? "email" : id === "linkedin" ? "url" : "string";
766
+ }
767
+ function applicationFlowFields(chapter, application = chapter.application) {
768
+ const defs = applicationDefs(chapter);
769
+ return [...application.required, ...application.optional].map((id, order) => {
770
+ const def = defs[id];
771
+ const conditions = application.conditions[id];
772
+ const type = applicationType(id, def);
773
+ return {
774
+ id,
775
+ label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
776
+ type,
777
+ required: application.required.includes(id),
778
+ order,
779
+ maxLength: application.maxLen[id] ?? application.defaultMaxLen,
780
+ ...type === "enum" && def?.options ? { options: [...def.options] } : {},
781
+ ...conditions ? { conditions: { ...conditions } } : {},
782
+ ...type === "email" ? { emailIdentity: "literal" } : {}
783
+ };
784
+ });
785
+ }
786
+ function resolveFormationFlowFields(crm, formation) {
787
+ if (!formation.enabled) return [];
788
+ const def = crm.type(formation.type);
789
+ return [...formation.required, ...formation.optional].map((id, order) => {
790
+ const field = def.fields[id];
791
+ return {
792
+ id,
793
+ label: field.label ?? humanize(id),
794
+ type: field.type,
795
+ required: formation.required.includes(id),
796
+ order,
797
+ maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
798
+ ...field.options ? { options: [...field.options] } : {},
799
+ ...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
800
+ };
801
+ });
802
+ }
803
+ function formationFlowFields(chapter, formation = chapter.formation) {
804
+ return resolveFormationFlowFields(chapter.crm, formation);
723
805
  }
724
- function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
806
+ function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
725
807
  return {
726
- meetingAt: startAt,
727
- ...htmlLink ? { meetingLink: htmlLink } : {},
728
- ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
808
+ schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
809
+ flow,
810
+ fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
729
811
  };
730
812
  }
731
813
 
@@ -762,8 +844,8 @@ function validateGiftOfferCatalog(input, tierIds) {
762
844
  errors.push(`${path} must be an object`);
763
845
  continue;
764
846
  }
765
- const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
766
- for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
847
+ const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
848
+ for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
767
849
  const id = text(item.id, `${path}.id`, errors, 80);
768
850
  if (id && !ID2.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
769
851
  if (ids.has(id)) errors.push(`${path}.id duplicates "${id}"`);
@@ -860,8 +942,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
860
942
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
861
943
  const page = Array.isArray(data) ? data : [];
862
944
  for (const u of page) {
863
- const record2 = toRecord(u);
864
- if (record2) out.push(record2);
945
+ const record3 = toRecord(u);
946
+ if (record3) out.push(record3);
865
947
  }
866
948
  if (page.length < PAGE) break;
867
949
  }
@@ -918,34 +1000,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
918
1000
  if (readable && raw.length <= 64) return raw;
919
1001
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
920
1002
  }
921
- function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
922
- if ("input" in record2 && record2.version === 2) {
1003
+ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
1004
+ if ("input" in record3 && record3.version === 2) {
923
1005
  return {
924
1006
  version: 2,
925
- sourceId: record2.source.siteId,
926
- sourceRecordId: record2.source.recordId,
927
- type: record2.type,
928
- input: record2.input
1007
+ sourceId: record3.source.siteId,
1008
+ sourceRecordId: record3.source.recordId,
1009
+ type: record3.type,
1010
+ input: record3.input
929
1011
  };
930
1012
  }
931
- if ("input" in record2) {
1013
+ if ("input" in record3) {
932
1014
  return {
933
1015
  version: 1,
934
1016
  sourceId: fallbackSourceId,
935
- sourceRecordId: record2.hubRecordId,
936
- type: record2.type,
937
- input: record2.input
1017
+ sourceRecordId: record3.hubRecordId,
1018
+ type: record3.type,
1019
+ input: record3.input
938
1020
  };
939
1021
  }
940
- if ("type" in record2 && record2.type === "company") {
941
- const input = { name: record2.name };
1022
+ if ("type" in record3 && record3.type === "company") {
1023
+ const input = { name: record3.name };
942
1024
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
943
- if (record2[key]) input[key] = record2[key];
1025
+ if (record3[key]) input[key] = record3[key];
944
1026
  }
945
1027
  return {
946
1028
  version: 1,
947
1029
  sourceId: fallbackSourceId,
948
- sourceRecordId: record2.hubRecordId,
1030
+ sourceRecordId: record3.hubRecordId,
949
1031
  type: "company",
950
1032
  input
951
1033
  };
@@ -953,28 +1035,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
953
1035
  return {
954
1036
  version: 1,
955
1037
  sourceId: fallbackSourceId,
956
- sourceRecordId: record2.hubRecordId,
1038
+ sourceRecordId: record3.hubRecordId,
957
1039
  type: "person",
958
- input: sharedPersonInput(record2)
1040
+ input: sharedPersonInput(record3)
959
1041
  };
960
1042
  }
961
- function sharedRecordFromCrm(crm, record2, target, sourceId) {
962
- if (target.fields && !target.fields[record2.type]) {
963
- throw new Error(`${target.name} does not accept "${record2.type}" records`);
1043
+ function sharedRecordFromCrm(crm, record3, target, sourceId) {
1044
+ if (target.fields && !target.fields[record3.type]) {
1045
+ throw new Error(`${target.name} does not accept "${record3.type}" records`);
964
1046
  }
965
- const def = crm.type(record2.type);
966
- const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
1047
+ const def = crm.type(record3.type);
1048
+ const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
967
1049
  if (!fields) {
968
- throw new Error(`${target.name} requires an explicit field allowlist for "${record2.type}" records`);
1050
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
969
1051
  }
970
1052
  const nameField = def.nameField ?? "name";
971
1053
  const input = {};
972
1054
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
973
- const value = record2.fields?.[field];
1055
+ const value = record3.fields?.[field];
974
1056
  if (value !== void 0) input[field] = value;
975
1057
  }
976
- if (input[nameField] === void 0) input[nameField] = record2.name;
977
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
1058
+ if (input[nameField] === void 0) input[nameField] = record3.name;
1059
+ return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
978
1060
  }
979
1061
  async function upsertPerson(deps, opts) {
980
1062
  const email = opts.email.toLowerCase();
@@ -988,11 +1070,11 @@ async function upsertPerson(deps, opts) {
988
1070
  const created = await (0, import_crm2.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
989
1071
  return { recordId: created.id };
990
1072
  }
991
- async function findSharedRecord(deps, record2, tag) {
1073
+ async function findSharedRecord(deps, record3, tag) {
992
1074
  const structured = await (0, import_crm2.getRecordByOrigin)(
993
1075
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
994
- record2.sourceId,
995
- record2.sourceRecordId
1076
+ record3.sourceId,
1077
+ record3.sourceRecordId
996
1078
  );
997
1079
  if (structured) return structured;
998
1080
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -1001,53 +1083,53 @@ async function findSharedRecord(deps, record2, tag) {
1001
1083
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1002
1084
  if (found.crm_record?.[0]) return found.crm_record[0];
1003
1085
  }
1004
- const def = deps.crm.type(record2.type);
1086
+ const def = deps.crm.type(record3.type);
1005
1087
  const emailField = def.emailField;
1006
- if (emailField && typeof record2.input[emailField] === "string") {
1007
- const primaryEmail = record2.input[emailField].toLowerCase();
1088
+ if (emailField && typeof record3.input[emailField] === "string") {
1089
+ const primaryEmail = record3.input[emailField].toLowerCase();
1008
1090
  const found = await deps.db.query({
1009
- crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
1091
+ crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
1010
1092
  });
1011
1093
  if (found.crm_record?.[0]) return found.crm_record[0];
1012
1094
  }
1013
- const domain = record2.input.domain;
1095
+ const domain = record3.input.domain;
1014
1096
  const domainSlot = def.fields.domain?.slot;
1015
1097
  if (typeof domain === "string" && domainSlot) {
1016
1098
  const found = await deps.db.query({
1017
- crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
1099
+ crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
1018
1100
  });
1019
1101
  if (found.crm_record?.[0]) return found.crm_record[0];
1020
1102
  }
1021
1103
  const nameField = def.nameField ?? "name";
1022
- const name = record2.input[nameField];
1023
- if (record2.type === "company" && typeof name === "string" && name.trim()) {
1104
+ const name = record3.input[nameField];
1105
+ if (record3.type === "company" && typeof name === "string" && name.trim()) {
1024
1106
  const found = await deps.db.query({
1025
- crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
1107
+ crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
1026
1108
  });
1027
1109
  if (found.crm_record?.[0]) return found.crm_record[0];
1028
1110
  }
1029
1111
  return void 0;
1030
1112
  }
1031
1113
  async function projectSharedRecord(deps, shared, options = {}) {
1032
- const record2 = normalizeSharedRecord(shared, options.sourceId);
1033
- if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
1114
+ const record3 = normalizeSharedRecord(shared, options.sourceId);
1115
+ if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
1034
1116
  throw new Error("type, source site id, and source record id must be non-empty");
1035
1117
  }
1036
- if (options.sourceId && options.sourceId !== record2.sourceId) {
1118
+ if (options.sourceId && options.sourceId !== record3.sourceId) {
1037
1119
  throw new Error("signed sender does not match payload source");
1038
1120
  }
1039
- const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
1121
+ const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
1040
1122
  const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1041
- const existing = await findSharedRecord(deps, record2, tag);
1123
+ const existing = await findSharedRecord(deps, record3, tag);
1042
1124
  let recordId;
1043
1125
  if (existing && typeof existing.id === "string") {
1044
- await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record2.input });
1126
+ await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record3.input });
1045
1127
  recordId = existing.id;
1046
1128
  } else {
1047
- recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
1129
+ recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
1048
1130
  await (0, import_crm2.createRecord)({ ...crmDeps3, newId: () => recordId }, {
1049
- type: record2.type,
1050
- input: record2.input,
1131
+ type: record3.type,
1132
+ input: record3.input,
1051
1133
  mutationId: `share-create:${tag}`
1052
1134
  });
1053
1135
  }
@@ -1057,10 +1139,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
1057
1139
  );
1058
1140
  await (0, import_crm2.upsertRecordOrigin)(crmDeps3, {
1059
1141
  recordId,
1060
- sourceId: record2.sourceId,
1061
- sourceRecordId: record2.sourceRecordId,
1142
+ sourceId: record3.sourceId,
1143
+ sourceRecordId: record3.sourceRecordId,
1062
1144
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
1063
- payloadVersion: record2.version
1145
+ payloadVersion: record3.version
1064
1146
  });
1065
1147
  return { recordId };
1066
1148
  }
@@ -1509,7 +1591,8 @@ function applicationSummary(app) {
1509
1591
  meetingLink: app.meetingLink ?? null,
1510
1592
  paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
1511
1593
  renewalAt: app.renewalAt ?? null,
1512
- canceled: app.canceled === true
1594
+ canceled: app.canceled === true,
1595
+ tierId: app.tierId ?? null
1513
1596
  };
1514
1597
  }
1515
1598
  function memberApplication(app, meeting, defaultTimezone) {
@@ -1733,7 +1816,7 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
1733
1816
  // src/worker-routes.ts
1734
1817
  async function memberSessionApplication(db, chapterId, email) {
1735
1818
  let apps = (await db.query({ applications: { $: {
1736
- where: { emailNormalized: normalizeAdmissionEmail(email) },
1819
+ where: { emailNormalized: normalizeChapterEmail(email) },
1737
1820
  limit: 20
1738
1821
  } } })).applications;
1739
1822
  if (!apps?.length) apps = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
@@ -1832,6 +1915,7 @@ var handleMember = async (req, url, env, ctx) => {
1832
1915
  const application = controlledApplication(chapter.application, activeControl?.document);
1833
1916
  return json({
1834
1917
  ...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
1918
+ fieldContract: chapterFlowFieldContract(chapter, "join", application),
1835
1919
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
1836
1920
  ...activeControl ? { signupControl: {
1837
1921
  revision: activeControl.revision,
@@ -1983,8 +2067,8 @@ var handleNetworkShared = async (req, url, env, ctx) => {
1983
2067
  payload,
1984
2068
  sourceId ? { sourceId } : {}
1985
2069
  );
1986
- const record2 = normalizeSharedRecord(payload, sourceId);
1987
- return json({ recordId, type: record2.type });
2070
+ const record3 = normalizeSharedRecord(payload, sourceId);
2071
+ return json({ recordId, type: record3.type });
1988
2072
  } catch (error) {
1989
2073
  return json({
1990
2074
  error: error instanceof Error ? error.message : "invalid shared record"
@@ -2024,9 +2108,9 @@ var MAX_BENEFITS = 24;
2024
2108
  var MAX_MARKDOWN = 65536;
2025
2109
  var MAX_LABEL = 160;
2026
2110
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2027
- function rejectUnknownKeys(value, allowed, path, errors) {
2111
+ function rejectUnknownKeys(value, allowed2, path, errors) {
2028
2112
  for (const key of Object.keys(value)) {
2029
- if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2113
+ if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2030
2114
  }
2031
2115
  }
2032
2116
  function boundedString(value, path, errors, max, required = false) {
@@ -2182,8 +2266,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2182
2266
  ]);
2183
2267
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2184
2268
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2185
- function unknownKeys(value, allowed, path, errors) {
2186
- for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2269
+ function unknownKeys(value, allowed2, path, errors) {
2270
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2187
2271
  }
2188
2272
  function text2(value, path, errors, max = 65536) {
2189
2273
  if (typeof value !== "string" || !value.trim()) {
@@ -2268,10 +2352,10 @@ function policyOf(value, errors) {
2268
2352
  errors.push("document.policy must be an object");
2269
2353
  return void 0;
2270
2354
  }
2271
- const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2272
- unknownKeys(input, allowed, "document.policy", errors);
2355
+ const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2356
+ unknownKeys(input, allowed2, "document.policy", errors);
2273
2357
  const result = {};
2274
- for (const key of allowed) {
2358
+ for (const key of allowed2) {
2275
2359
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
2276
2360
  result[key] = text2(input[key], `document.policy.${key}`, errors);
2277
2361
  }
@@ -2410,9 +2494,589 @@ var handleSignupControl = async (req, url, env, ctx) => {
2410
2494
  });
2411
2495
  };
2412
2496
 
2413
- // src/worker-routes-admission-grants.ts
2497
+ // src/worker-routes-member-content.ts
2414
2498
  var import_db4 = require("@odla-ai/db");
2499
+
2500
+ // src/member-content-canonical.ts
2501
+ var encoder2 = new TextEncoder();
2502
+ function canonicalValue2(value) {
2503
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2504
+ return JSON.stringify(value);
2505
+ }
2506
+ if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
2507
+ if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
2508
+ const record3 = value;
2509
+ const entries = Object.keys(record3).sort().flatMap((key) => {
2510
+ const item = record3[key];
2511
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
2512
+ });
2513
+ return `{${entries.join(",")}}`;
2514
+ }
2515
+ function canonicalMemberContent(value) {
2516
+ return canonicalValue2(value);
2517
+ }
2518
+ async function memberContentDigest(document) {
2519
+ const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
2520
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2521
+ }
2522
+
2523
+ // src/member-content-validation-helpers.ts
2524
+ var ID3 = /^[a-z][a-z0-9-]{0,79}$/;
2525
+ var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2526
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2527
+ var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
2528
+ var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
2529
+ var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
2530
+ var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
2531
+ var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
2532
+ var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
2533
+ var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
2534
+ var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
2535
+ var MAX_PAGES = 100;
2536
+ var MAX_MARKDOWN2 = 65536;
2537
+ var MAX_TEXT = 240;
2538
+ var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2539
+ function unknownKeys2(value, allowed2, path, errors) {
2540
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2541
+ }
2542
+ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
2543
+ if (value === void 0) {
2544
+ if (required) errors.push(`${path} is required`);
2545
+ return void 0;
2546
+ }
2547
+ if (typeof value !== "string") {
2548
+ errors.push(`${path} must be a string`);
2549
+ return void 0;
2550
+ }
2551
+ const valueText = value.trim();
2552
+ if (required && !valueText) errors.push(`${path} is required`);
2553
+ if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2554
+ return valueText;
2555
+ }
2556
+ function offerValue(value, path, errors, tiers, audience) {
2557
+ const raw = record2(value);
2558
+ if (!raw) {
2559
+ errors.push(`${path} must be an object`);
2560
+ return void 0;
2561
+ }
2562
+ unknownKeys2(raw, OFFER_KEYS, path, errors);
2563
+ const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2564
+ if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2565
+ if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2566
+ if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2567
+ errors.push(`${path}.tierId must grant access to this page`);
2568
+ }
2569
+ const label = textValue(raw.label, `${path}.label`, errors);
2570
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2);
2571
+ const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
2572
+ return {
2573
+ tierId,
2574
+ ...label ? { label } : {},
2575
+ ...bodyMarkdown ? { bodyMarkdown } : {},
2576
+ ...ctaLabel ? { ctaLabel } : {}
2577
+ };
2578
+ }
2579
+
2580
+ // src/member-content-validation.ts
2581
+ function validateMemberContentDocument(input, options = {}) {
2582
+ const errors = [];
2583
+ const root = record2(input);
2584
+ if (!root) return { ok: false, errors: ["document must be an object"] };
2585
+ unknownKeys2(root, ROOT_KEYS3, "document", errors);
2586
+ if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
2587
+ if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
2588
+ if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
2589
+ const sourceRaw = record2(root.source);
2590
+ let source = { type: "chapter", appId: "invalid" };
2591
+ if (!sourceRaw) errors.push("document.source must be an object");
2592
+ else {
2593
+ unknownKeys2(sourceRaw, SOURCE_KEYS, "document.source", errors);
2594
+ const type = sourceRaw.type;
2595
+ const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
2596
+ if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
2597
+ if (appId && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
2598
+ if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2599
+ if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
2600
+ if (type === "parent" || type === "chapter") source = { type, appId };
2601
+ }
2602
+ let target;
2603
+ if (root.target !== void 0) {
2604
+ const targetRaw = record2(root.target);
2605
+ if (!targetRaw) errors.push("document.target must be an object");
2606
+ else {
2607
+ unknownKeys2(targetRaw, TARGET_KEYS, "document.target", errors);
2608
+ const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
2609
+ const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
2610
+ target = { chapterId, environment };
2611
+ }
2612
+ }
2613
+ if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
2614
+ if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
2615
+ if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
2616
+ const rawPages = Array.isArray(root.pages) ? root.pages : [];
2617
+ if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
2618
+ const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
2619
+ const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
2620
+ const ids = /* @__PURE__ */ new Set();
2621
+ const slugs = /* @__PURE__ */ new Set();
2622
+ const pages = [];
2623
+ for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2624
+ const path = `document.pages[${index}]`;
2625
+ const raw = record2(value);
2626
+ if (!raw) {
2627
+ errors.push(`${path} must be an object`);
2628
+ continue;
2629
+ }
2630
+ unknownKeys2(raw, PAGE_KEYS, path, errors);
2631
+ const id = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
2632
+ const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
2633
+ const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2634
+ if (id && !ID3.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2635
+ if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2636
+ if (ids.has(id) || knownParentIds.has(id)) errors.push(`${path}.id duplicates page "${id}"`);
2637
+ if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
2638
+ ids.add(id);
2639
+ slugs.add(slug);
2640
+ const navRaw = record2(raw.navigation);
2641
+ let navigation = { section: "General", order: index };
2642
+ if (!navRaw) errors.push(`${path}.navigation must be an object`);
2643
+ else {
2644
+ unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
2645
+ const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
2646
+ const order = navRaw.order;
2647
+ if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
2648
+ errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
2649
+ }
2650
+ const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
2651
+ navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
2652
+ }
2653
+ const audienceRaw = record2(raw.audience);
2654
+ let audience = { kind: "all-members" };
2655
+ if (!audienceRaw) errors.push(`${path}.audience must be an object`);
2656
+ else {
2657
+ unknownKeys2(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
2658
+ if (audienceRaw.kind === "all-members") {
2659
+ if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
2660
+ } else if (audienceRaw.kind === "tiers") {
2661
+ if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
2662
+ errors.push(`${path}.audience.tierIds must be a non-empty array`);
2663
+ }
2664
+ const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
2665
+ const normalized = [];
2666
+ for (const [tierIndex, value2] of values.entries()) {
2667
+ const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
2668
+ if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
2669
+ if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
2670
+ normalized.push(tier);
2671
+ }
2672
+ audience = { kind: "tiers", tierIds: normalized };
2673
+ } else errors.push(`${path}.audience.kind must be all-members or tiers`);
2674
+ }
2675
+ const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
2676
+ const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
2677
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
2678
+ let upsell;
2679
+ if (raw.upsell !== void 0) {
2680
+ const upsellRaw = record2(raw.upsell);
2681
+ if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
2682
+ else {
2683
+ unknownKeys2(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
2684
+ const owner = upsellRaw.owner;
2685
+ if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
2686
+ if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
2687
+ if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
2688
+ if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
2689
+ const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
2690
+ if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
2691
+ else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
2692
+ }
2693
+ }
2694
+ pages.push({ id, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
2695
+ }
2696
+ const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
2697
+ for (const [index, page] of pages.entries()) {
2698
+ const parentId = page.navigation.parentId;
2699
+ if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
2700
+ if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
2701
+ }
2702
+ for (const page of pages) {
2703
+ const seen = /* @__PURE__ */ new Set([page.id]);
2704
+ let parentId = page.navigation.parentId;
2705
+ while (parentId) {
2706
+ if (seen.has(parentId)) {
2707
+ errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
2708
+ break;
2709
+ }
2710
+ seen.add(parentId);
2711
+ parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
2712
+ }
2713
+ }
2714
+ let upsellOverrides;
2715
+ if (root.upsellOverrides !== void 0) {
2716
+ if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
2717
+ if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
2718
+ const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
2719
+ const seen = /* @__PURE__ */ new Set();
2720
+ upsellOverrides = [];
2721
+ for (const [index, value] of values.entries()) {
2722
+ const path = `document.upsellOverrides[${index}]`;
2723
+ const raw = record2(value);
2724
+ if (!raw) {
2725
+ errors.push(`${path} must be an object`);
2726
+ continue;
2727
+ }
2728
+ unknownKeys2(raw, OVERRIDE_KEYS, path, errors);
2729
+ const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
2730
+ if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
2731
+ seen.add(pageId);
2732
+ const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
2733
+ if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
2734
+ else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
2735
+ const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
2736
+ if (offer) upsellOverrides.push({ pageId, offer });
2737
+ }
2738
+ }
2739
+ if (errors.length) return { ok: false, errors };
2740
+ return { ok: true, value: {
2741
+ schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
2742
+ revision: Number(root.revision),
2743
+ generatedAt: Number(root.generatedAt),
2744
+ source,
2745
+ ...target ? { target } : {},
2746
+ pages,
2747
+ ...upsellOverrides ? { upsellOverrides } : {}
2748
+ } };
2749
+ }
2750
+
2751
+ // src/member-content.ts
2752
+ var MEMBER_CONTENT_SCHEMA_VERSION = 1;
2753
+ var MEMBER_CONTENT_PATH = "/api/member-content";
2754
+ var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
2755
+ 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);
2756
+ function composeMemberContent(parent, chapter) {
2757
+ const pages = [];
2758
+ const parentPages = /* @__PURE__ */ new Map();
2759
+ for (const page of parent?.pages ?? []) {
2760
+ const effective = { ...page, source: parent.source };
2761
+ parentPages.set(page.id, effective);
2762
+ pages.push(effective);
2763
+ }
2764
+ for (const override of chapter?.upsellOverrides ?? []) {
2765
+ const page = parentPages.get(override.pageId);
2766
+ if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
2767
+ }
2768
+ for (const page of chapter?.pages ?? []) {
2769
+ if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
2770
+ throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
2771
+ }
2772
+ pages.push({ ...page, source: chapter.source });
2773
+ }
2774
+ return {
2775
+ pages: pages.sort(pageOrder),
2776
+ parentRevision: parent?.revision ?? null,
2777
+ chapterRevision: chapter?.revision ?? null
2778
+ };
2779
+ }
2780
+ var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
2781
+ function projectMemberContent(content, viewer, tiers) {
2782
+ const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
2783
+ return {
2784
+ viewer: { ...viewer },
2785
+ revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
2786
+ pages: content.pages.map((page) => {
2787
+ const canRead = allowed(page, viewer);
2788
+ const offer = !canRead ? page.upsell?.offer : void 0;
2789
+ const tier = offer ? authority.get(offer.tierId) : void 0;
2790
+ const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
2791
+ return {
2792
+ id: page.id,
2793
+ slug: page.slug,
2794
+ title: page.title,
2795
+ ...page.summary ? { summary: page.summary } : {},
2796
+ navigation: page.navigation,
2797
+ audience: page.audience,
2798
+ source: page.source,
2799
+ access: canRead ? "allowed" : "locked",
2800
+ ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
2801
+ ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
2802
+ ...upsell ? { upsell } : {}
2803
+ };
2804
+ })
2805
+ };
2806
+ }
2807
+
2808
+ // src/member-content-store.ts
2809
+ var control = (row) => row && row.document && typeof row.document === "object" ? row : null;
2810
+ var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
2811
+ async function exists(db, ns, where) {
2812
+ if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
2813
+ const rows4 = (await db.query({ [ns]: {} }))[ns] ?? [];
2814
+ return rows4.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
2815
+ }
2816
+ async function loadActiveMemberContentControl(db, targetId, environment) {
2817
+ const where = { targetId, environment, active: true };
2818
+ if (!await exists(db, "memberContentControls", where)) return null;
2819
+ const rows4 = (await db.query({ memberContentControls: { $: {
2820
+ where,
2821
+ order: { revision: "desc" },
2822
+ limit: 1
2823
+ } } })).memberContentControls;
2824
+ return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
2825
+ }
2826
+ async function loadLatestMemberContentControl(db, targetId, environment) {
2827
+ const where = { targetId, environment };
2828
+ if (!await exists(db, "memberContentControls", where)) return null;
2829
+ const rows4 = (await db.query({ memberContentControls: { $: {
2830
+ where,
2831
+ order: { revision: "desc" },
2832
+ limit: 1
2833
+ } } })).memberContentControls;
2834
+ return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
2835
+ }
2836
+ async function loadLocalMemberContent(db, chapterId, environment) {
2837
+ const id = `member-content-local:${environment}`;
2838
+ const rows4 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
2839
+ return local(Array.isArray(rows4) ? rows4[0] : void 0);
2840
+ }
2841
+ async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
2842
+ const { document, digest } = envelope;
2843
+ const target = document.target;
2844
+ const id = `member-content-control:${target.environment}:${document.revision}`;
2845
+ const ops = [];
2846
+ if (previous?.active) {
2847
+ ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
2848
+ }
2849
+ ops.push({ t: "update", ns: "memberContentControls", id, attrs: {
2850
+ id,
2851
+ sourceId: document.source.appId,
2852
+ targetId: target.chapterId,
2853
+ environment: target.environment,
2854
+ revision: document.revision,
2855
+ digest,
2856
+ document,
2857
+ active: true,
2858
+ receivedAt
2859
+ } });
2860
+ await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
2861
+ }
2862
+ async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
2863
+ const id = `member-content-local:${environment}`;
2864
+ const current = await loadLocalMemberContent(db, document.source.appId, environment);
2865
+ if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
2866
+ const row = {
2867
+ id,
2868
+ chapterId: document.source.appId,
2869
+ environment,
2870
+ revision: document.revision,
2871
+ document,
2872
+ updatedAt,
2873
+ ...updatedBy ? { updatedBy } : {}
2874
+ };
2875
+ await db.transact([{ t: "update", ns: "memberContentLocal", id, attrs: row }], {
2876
+ mutationId: `member-content-local:${environment}:${document.revision}`,
2877
+ guards: current ? [{ ns: "memberContentLocal", id, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id, exists: false }]
2878
+ });
2879
+ return row;
2880
+ }
2881
+
2882
+ // src/worker-routes-member-content.ts
2883
+ var ADMIN_PATH = "/api/admin/member-content";
2884
+ var BODY_CAP = 1e6;
2415
2885
  var reject2 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2886
+ async function groupAndTiers(db, chapterId) {
2887
+ const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
2888
+ const group = Array.isArray(groups) ? groups[0] : void 0;
2889
+ if (!group) return { group: null, tiers: [] };
2890
+ return { group, tiers: await loadTiers(db, group) };
2891
+ }
2892
+ async function memberTierId(db, chapterId, email) {
2893
+ if (!email) return null;
2894
+ let rows4 = (await db.query({ applications: { $: {
2895
+ where: { emailNormalized: normalizeAdmissionEmail(email) },
2896
+ limit: 20
2897
+ } } })).applications;
2898
+ if (!rows4?.length) rows4 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
2899
+ const latest = Array.isArray(rows4) ? [...rows4].filter((row) => String(row.groupId ?? chapterId) === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0] : void 0;
2900
+ return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
2901
+ }
2902
+ async function activeDocuments(db, chapterId, environment) {
2903
+ const [parent, chapter] = await Promise.all([
2904
+ loadActiveMemberContentControl(db, chapterId, environment),
2905
+ loadLocalMemberContent(db, chapterId, environment)
2906
+ ]);
2907
+ return { parent, chapter };
2908
+ }
2909
+ var handleMemberContentControl = async (req, url, env, ctx) => {
2910
+ if (req.method !== "POST" || url.pathname !== MEMBER_CONTENT_CONTROL_PATH) return null;
2911
+ const settings = ctx.chapter.memberContent?.parent;
2912
+ if (!settings) return json({ error: "not found" }, 404);
2913
+ const db = ctx.makeDb(env);
2914
+ const secret = await getVaultSecret(db, settings.secretName);
2915
+ if (!secret) return json({ error: "unauthorized" }, 401);
2916
+ const verified = await (0, import_db4.verifyFederatedRequest)(req, { secret, senders: [settings.sourceId] });
2917
+ if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2918
+ let input;
2919
+ try {
2920
+ input = JSON.parse(verified.body);
2921
+ } catch {
2922
+ return reject2("invalid_document", 400, ["request body must be JSON"]);
2923
+ }
2924
+ const digest = typeof input.digest === "string" ? input.digest : "";
2925
+ const initial = validateMemberContentDocument(input.document, {
2926
+ sourceType: "parent"
2927
+ });
2928
+ if (!initial.ok) return reject2("invalid_document", 400, initial.errors);
2929
+ const document = initial.value;
2930
+ if (document.source.appId !== settings.sourceId) return reject2("wrong_source", 403);
2931
+ if (document.target?.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
2932
+ if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
2933
+ const { tiers } = await groupAndTiers(db, ctx.chapter.id);
2934
+ const checked = validateMemberContentDocument(document, {
2935
+ sourceType: "parent",
2936
+ sourceAppId: settings.sourceId,
2937
+ tiers
2938
+ });
2939
+ if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
2940
+ const local2 = await loadLocalMemberContent(db, ctx.chapter.id, env.ODLA_ENV);
2941
+ if (local2) {
2942
+ const localCheck = validateMemberContentDocument(local2.document, {
2943
+ sourceType: "chapter",
2944
+ sourceAppId: ctx.chapter.id,
2945
+ tiers,
2946
+ parentPages: document.pages
2947
+ });
2948
+ if (!localCheck.ok) {
2949
+ return reject2("invalid_document", 409, localCheck.errors.map((error) => `existing chapter content: ${error}`));
2950
+ }
2951
+ }
2952
+ if (!digest || await memberContentDigest(document) !== digest) return reject2("invalid_digest", 400);
2953
+ const latest = await loadLatestMemberContentControl(db, ctx.chapter.id, env.ODLA_ENV);
2954
+ if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
2955
+ if (latest && document.revision === latest.revision) {
2956
+ if (latest.digest !== digest) return reject2("conflicting_revision", 409);
2957
+ return json({
2958
+ ok: true,
2959
+ target: document.target,
2960
+ revision: latest.revision,
2961
+ digest: latest.digest,
2962
+ status: "replayed",
2963
+ acknowledgedAt: latest.receivedAt
2964
+ });
2965
+ }
2966
+ const receivedAt = Date.now();
2967
+ try {
2968
+ await materializeMemberContentControl(db, { digest, document }, latest, receivedAt);
2969
+ } catch {
2970
+ return reject2("materialization_failed", 503);
2971
+ }
2972
+ return json({
2973
+ ok: true,
2974
+ target: document.target,
2975
+ revision: document.revision,
2976
+ digest,
2977
+ status: "accepted",
2978
+ acknowledgedAt: receivedAt
2979
+ });
2980
+ };
2981
+ var handleMemberContent = async (req, url, env, ctx) => {
2982
+ if (req.method !== "GET" || url.pathname !== MEMBER_CONTENT_PATH) return null;
2983
+ if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
2984
+ const user = await ctx.verifyUser(req, env);
2985
+ if (!user) return json({ error: "unauthorized" }, 401);
2986
+ const db = ctx.makeDb(env);
2987
+ const role = await ctx.roleFor(db, user);
2988
+ const admin = await ctx.isAdmin(db, user);
2989
+ const roleIndex = ctx.chapter.auth.ladder.indexOf(role);
2990
+ const memberIndex = Math.max(0, ctx.chapter.auth.ladder.length - 2);
2991
+ if (!admin && roleIndex < memberIndex) return json({ error: "member access required" }, 403);
2992
+ const [{ parent, chapter }, { group, tiers }, tierId] = await Promise.all([
2993
+ activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
2994
+ groupAndTiers(db, ctx.chapter.id),
2995
+ memberTierId(db, ctx.chapter.id, user.email)
2996
+ ]);
2997
+ const effective = composeMemberContent(parent?.document, chapter?.document);
2998
+ let offerable = tiers;
2999
+ if (group) {
3000
+ const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
3001
+ offerable = await loadOfferableTiers(db, group, Boolean(stripeSecret));
3002
+ }
3003
+ return json(projectMemberContent(effective, { tierId, admin }, offerable));
3004
+ };
3005
+ var handleAdminMemberContent = async (req, url, env, ctx) => {
3006
+ if (url.pathname !== ADMIN_PATH) return null;
3007
+ if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
3008
+ const user = await ctx.verifyUser(req, env);
3009
+ if (!user) return json({ error: "unauthorized" }, 401);
3010
+ const db = ctx.makeDb(env);
3011
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
3012
+ const [{ parent, chapter }, { tiers }] = await Promise.all([
3013
+ activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
3014
+ groupAndTiers(db, ctx.chapter.id)
3015
+ ]);
3016
+ if (req.method === "GET") {
3017
+ const effective = composeMemberContent(parent?.document, chapter?.document);
3018
+ return json({
3019
+ parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
3020
+ chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
3021
+ effective,
3022
+ tiers: tiers.map(({ id, name, priceCents, active }) => ({ id, name, priceCents, active })),
3023
+ localAuthoring: ctx.chapter.memberContent.localAuthoring
3024
+ });
3025
+ }
3026
+ if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
3027
+ if (!ctx.chapter.memberContent.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
3028
+ const raw = await req.text();
3029
+ if (raw.length > BODY_CAP) return json({ error: "request body too large" }, 413);
3030
+ let input;
3031
+ try {
3032
+ input = JSON.parse(raw);
3033
+ } catch {
3034
+ return json({ error: "invalid JSON body" }, 400);
3035
+ }
3036
+ const expectedRevision = Number(input.expectedRevision);
3037
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 0) {
3038
+ return json({ error: "expectedRevision must be a non-negative integer" }, 400);
3039
+ }
3040
+ const now = Date.now();
3041
+ const documentInput = withLocalAuthority(input.document, expectedRevision + 1, now, ctx.chapter.id);
3042
+ const checked = validateMemberContentDocument(documentInput, {
3043
+ sourceType: "chapter",
3044
+ sourceAppId: ctx.chapter.id,
3045
+ tiers,
3046
+ parentPages: parent?.document.pages ?? []
3047
+ });
3048
+ if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
3049
+ try {
3050
+ const saved = await saveLocalMemberContent(
3051
+ db,
3052
+ checked.value,
3053
+ env.ODLA_ENV,
3054
+ expectedRevision,
3055
+ now,
3056
+ user.email ?? user.userId
3057
+ );
3058
+ return json({ ok: true, revision: saved.revision, document: saved.document });
3059
+ } catch (error) {
3060
+ if (error instanceof Error && error.message === "member_content_revision_conflict") {
3061
+ return json({ error: "member content changed; reload before saving" }, 409);
3062
+ }
3063
+ return json({ error: "member content could not be saved" }, 503);
3064
+ }
3065
+ };
3066
+ function withLocalAuthority(value, revision, generatedAt, appId) {
3067
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
3068
+ return {
3069
+ ...value,
3070
+ schemaVersion: 1,
3071
+ revision,
3072
+ generatedAt,
3073
+ source: { type: "chapter", appId }
3074
+ };
3075
+ }
3076
+
3077
+ // src/worker-routes-admission-grants.ts
3078
+ var import_db5 = require("@odla-ai/db");
3079
+ var reject3 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2416
3080
  var handleAdmissionGrants = async (req, url, env, ctx) => {
2417
3081
  if (req.method !== "POST" || url.pathname !== ADMISSION_GRANTS_PATH) return null;
2418
3082
  const authority = ctx.chapter.signupControl;
@@ -2420,26 +3084,26 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
2420
3084
  const db = ctx.makeDb(env);
2421
3085
  const secret = await getVaultSecret(db, authority.secretName);
2422
3086
  if (!secret) return json({ error: "unauthorized" }, 401);
2423
- const verified = await (0, import_db4.verifyFederatedRequest)(req, { secret, senders: [authority.sourceId] });
3087
+ const verified = await (0, import_db5.verifyFederatedRequest)(req, { secret, senders: [authority.sourceId] });
2424
3088
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2425
3089
  let input;
2426
3090
  try {
2427
3091
  input = JSON.parse(verified.body);
2428
3092
  } catch {
2429
- return reject2("invalid_document", 400, ["request body must be JSON"]);
3093
+ return reject3("invalid_document", 400, ["request body must be JSON"]);
2430
3094
  }
2431
3095
  const digest = typeof input.digest === "string" ? input.digest : "";
2432
3096
  const checked = validateAdmissionGrantDocument(input.document);
2433
- if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
3097
+ if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
2434
3098
  const document = checked.value;
2435
- if (document.source.appId !== authority.sourceId) return reject2("wrong_source", 403);
2436
- if (document.target.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
2437
- if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
2438
- if (!digest || await signupControlDigest(document) !== digest) return reject2("invalid_digest", 400);
3099
+ if (document.source.appId !== authority.sourceId) return reject3("wrong_source", 403);
3100
+ if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
3101
+ if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
3102
+ if (!digest || await signupControlDigest(document) !== digest) return reject3("invalid_digest", 400);
2439
3103
  const latest = await loadLatestAdmissionRevision(db, authority.sourceId, ctx.chapter.id, env.ODLA_ENV);
2440
- if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
3104
+ if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
2441
3105
  if (latest && document.revision === latest.revision) {
2442
- if (latest.digest !== digest) return reject2("conflicting_revision", 409);
3106
+ if (latest.digest !== digest) return reject3("conflicting_revision", 409);
2443
3107
  return json({
2444
3108
  ok: true,
2445
3109
  target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
@@ -2453,7 +3117,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
2453
3117
  try {
2454
3118
  await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
2455
3119
  } catch {
2456
- return reject2("materialization_failed", 503);
3120
+ return reject3("materialization_failed", 503);
2457
3121
  }
2458
3122
  return json({
2459
3123
  ok: true,
@@ -2495,7 +3159,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
2495
3159
  return json({ error: "invalid JSON body" }, 400);
2496
3160
  }
2497
3161
  const email = typeof body.email === "string" ? body.email.trim() : "";
2498
- const emailNormalized = normalizeAdmissionEmail(email);
3162
+ const emailNormalized = normalizeChapterEmail(email);
2499
3163
  if (!isValidEmail(emailNormalized)) return json({ error: "email must be valid" }, 400);
2500
3164
  const decision = body.decision ?? "waive_interview";
2501
3165
  if (decision !== "waive_interview" && decision !== "require_interview") {
@@ -2997,7 +3661,7 @@ async function rotateGiftClaim(db, claim, now) {
2997
3661
  async function redeemGiftClaim(db, claim, beneficiary, now) {
2998
3662
  if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) return null;
2999
3663
  const chapterId = String(claim.chapterId);
3000
- const emailNormalized = normalizeAdmissionEmail(beneficiary.email);
3664
+ const emailNormalized = normalizeChapterEmail(beneficiary.email);
3001
3665
  const target = claim.target;
3002
3666
  const ops = [];
3003
3667
  let result;
@@ -3281,8 +3945,8 @@ async function startSubscription(req, env, ctx) {
3281
3945
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
3282
3946
  if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
3283
3947
  if (typeof app.giftOfferId === "string") {
3284
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3285
- const offer = findGiftOffer(control?.document.giftOffers, app.giftOfferId, Date.now());
3948
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3949
+ const offer = findGiftOffer(control2?.document.giftOffers, app.giftOfferId, Date.now());
3286
3950
  if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
3287
3951
  return json({ error: "gift offer is unavailable" }, 409);
3288
3952
  }
@@ -3294,7 +3958,7 @@ async function startSubscription(req, env, ctx) {
3294
3958
  offer,
3295
3959
  purchaserType: "application",
3296
3960
  purchaserEmail: String(app.email ?? ""),
3297
- purchaserEmailNormalized: normalizeAdmissionEmail(String(app.email ?? "")),
3961
+ purchaserEmailNormalized: normalizeChapterEmail(String(app.email ?? "")),
3298
3962
  purchaserApplicationId: applicationId,
3299
3963
  stripeSecret: secretKey,
3300
3964
  publishableKey: group.stripePublishableKey,
@@ -3441,14 +4105,14 @@ var handleGifts = async (req, url, env, ctx) => {
3441
4105
  if (req.method === "GET" && url.pathname === "/api/gifts") {
3442
4106
  const user = await userFor(req, env, ctx);
3443
4107
  if (!user) return json({ error: "unauthorized" }, 401);
3444
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4108
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3445
4109
  const found = await db.query({ giftClaims: { $: { where: {
3446
- purchaserEmailNormalized: normalizeAdmissionEmail(user.email),
4110
+ purchaserEmailNormalized: normalizeChapterEmail(user.email),
3447
4111
  chapterId: ctx.chapter.id
3448
4112
  }, limit: 200 } } });
3449
4113
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3450
4114
  return json({
3451
- offers: publicGiftOffers(control?.document.giftOffers, Date.now()),
4115
+ offers: publicGiftOffers(control2?.document.giftOffers, Date.now()),
3452
4116
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
3453
4117
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
3454
4118
  });
@@ -3464,13 +4128,13 @@ var handleGifts = async (req, url, env, ctx) => {
3464
4128
  }
3465
4129
  const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
3466
4130
  if (!/^[A-Za-z0-9_-]{16,128}$/.test(purchaseId)) return json({ error: "purchaseId is invalid" }, 400);
3467
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3468
- const offer = findGiftOffer(control?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
4131
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4132
+ const offer = findGiftOffer(control2?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
3469
4133
  if (!offer) return json({ error: "gift offer is unavailable" }, 409);
3470
4134
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3471
4135
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
3472
4136
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
3473
- const emailNormalized = normalizeAdmissionEmail(user.email);
4137
+ const emailNormalized = normalizeChapterEmail(user.email);
3474
4138
  const applications = rows2((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
3475
4139
  const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
3476
4140
  try {
@@ -3512,7 +4176,7 @@ var handleGifts = async (req, url, env, ctx) => {
3512
4176
  return invalid();
3513
4177
  }
3514
4178
  }
3515
- const owns = user ? claim.purchaserEmailNormalized === normalizeAdmissionEmail(user.email) : claim.purchaserApplicationId === applicationId;
4179
+ const owns = user ? claim.purchaserEmailNormalized === normalizeChapterEmail(user.email) : claim.purchaserApplicationId === applicationId;
3516
4180
  if (!owns) return invalid();
3517
4181
  const rotated = await rotateGiftClaim(db, claim, Date.now());
3518
4182
  return rotated ? json(rotated) : invalid();
@@ -4386,7 +5050,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
4386
5050
 
4387
5051
  // src/worker-routes-network.ts
4388
5052
  var import_crm6 = require("@odla-ai/crm");
4389
- var import_db6 = require("@odla-ai/db");
5053
+ var import_db7 = require("@odla-ai/db");
4390
5054
 
4391
5055
  // src/worker-network-fetch.ts
4392
5056
  function targetFetcher(env, target) {
@@ -4404,8 +5068,8 @@ function fetchNetworkTarget(env, target, destination, init) {
4404
5068
 
4405
5069
  // src/worker-network-push.ts
4406
5070
  var import_crm5 = require("@odla-ai/crm");
4407
- var import_db5 = require("@odla-ai/db");
4408
- async function pushNetworkRecord(db, env, ctx, target, record2) {
5071
+ var import_db6 = require("@odla-ai/db");
5072
+ async function pushNetworkRecord(db, env, ctx, target, record3) {
4409
5073
  const secret = await getVaultSecret(db, target.secretName);
4410
5074
  const crmDeps3 = {
4411
5075
  crm: ctx.chapter.crm,
@@ -4416,7 +5080,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4416
5080
  if (!secret) {
4417
5081
  const error = `vault secret "${target.secretName}" is missing`;
4418
5082
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
4419
- recordId: record2.id,
5083
+ recordId: record3.id,
4420
5084
  targetId: target.id,
4421
5085
  status: "failed",
4422
5086
  payloadVersion: 2,
@@ -4426,7 +5090,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4426
5090
  }
4427
5091
  let payload;
4428
5092
  try {
4429
- payload = sharedRecordFromCrm(ctx.chapter.crm, record2, target, ctx.chapter.id);
5093
+ payload = sharedRecordFromCrm(ctx.chapter.crm, record3, target, ctx.chapter.id);
4430
5094
  } catch (err) {
4431
5095
  return {
4432
5096
  id: target.id,
@@ -4438,7 +5102,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4438
5102
  const payloadBody = JSON.stringify(payload);
4439
5103
  const destination = new URL("/api/network/shared", target.url);
4440
5104
  try {
4441
- const signed = await (0, import_db5.signFederatedRequest)({
5105
+ const signed = await (0, import_db6.signFederatedRequest)({
4442
5106
  secret,
4443
5107
  sender: ctx.chapter.id,
4444
5108
  method: "POST",
@@ -4455,7 +5119,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4455
5119
  if (!res.ok) {
4456
5120
  const error = responseBody.error ?? "follower rejected the record";
4457
5121
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
4458
- recordId: record2.id,
5122
+ recordId: record3.id,
4459
5123
  targetId: target.id,
4460
5124
  status: "failed",
4461
5125
  payloadVersion: 2,
@@ -4470,7 +5134,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4470
5134
  };
4471
5135
  }
4472
5136
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
4473
- recordId: record2.id,
5137
+ recordId: record3.id,
4474
5138
  targetId: target.id,
4475
5139
  status: "delivered",
4476
5140
  payloadVersion: 2,
@@ -4486,7 +5150,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4486
5150
  } catch (err) {
4487
5151
  const error = err instanceof Error ? err.message : "delivery failed";
4488
5152
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
4489
- recordId: record2.id,
5153
+ recordId: record3.id,
4490
5154
  targetId: target.id,
4491
5155
  status: "failed",
4492
5156
  payloadVersion: 2,
@@ -4523,7 +5187,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
4523
5187
  const db = ctx.makeDb(env);
4524
5188
  const secret = await getVaultSecret(db, "network_share_secret");
4525
5189
  if (!secret) return json({ error: "unauthorized" }, 401);
4526
- const verified = await (0, import_db6.verifyFederatedRequest)(req, { secret });
5190
+ const verified = await (0, import_db7.verifyFederatedRequest)(req, { secret });
4527
5191
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
4528
5192
  const types = await Promise.all(
4529
5193
  Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm6.typeSummary)({ crm: ctx.chapter.crm, db }, type))
@@ -4541,7 +5205,7 @@ async function snapshotOne(db, env, ctx, target) {
4541
5205
  if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
4542
5206
  const destination = new URL("/api/network/snapshot", target.url);
4543
5207
  try {
4544
- const headers = await (0, import_db6.signFederatedRequest)({
5208
+ const headers = await (0, import_db7.signFederatedRequest)({
4545
5209
  secret,
4546
5210
  sender: ctx.chapter.id,
4547
5211
  method: "GET",
@@ -4624,17 +5288,17 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
4624
5288
  if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
4625
5289
  const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
4626
5290
  if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
4627
- const record2 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
4628
- if (!record2) return json({ error: "record not found" }, 404);
5291
+ const record3 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
5292
+ if (!record3) return json({ error: "record not found" }, 404);
4629
5293
  const results = await Promise.all(
4630
- targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record2))
5294
+ targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record3))
4631
5295
  );
4632
5296
  return json({ ok: results.every((result) => result.ok), results });
4633
5297
  };
4634
5298
 
4635
5299
  // src/worker-routes-network-records.ts
4636
5300
  var import_crm7 = require("@odla-ai/crm");
4637
- var import_db7 = require("@odla-ai/db");
5301
+ var import_db8 = require("@odla-ai/db");
4638
5302
  var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
4639
5303
  var MAX_PAGE = 50;
4640
5304
  var MAX_OFFSET = 1e4;
@@ -4652,18 +5316,18 @@ function integer(value, fallback, min, max) {
4652
5316
  const parsed = Number(value);
4653
5317
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
4654
5318
  }
4655
- function projectRecord(record2, allowed) {
5319
+ function projectRecord(record3, allowed2) {
4656
5320
  const fields = Object.fromEntries(
4657
- allowed.flatMap((name) => Object.prototype.hasOwnProperty.call(record2.fields, name) ? [[name, record2.fields[name]]] : [])
5321
+ allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record3.fields, name) ? [[name, record3.fields[name]]] : [])
4658
5322
  );
4659
5323
  return {
4660
- id: record2.id,
4661
- type: record2.type,
4662
- name: record2.name,
4663
- ...record2.stage ? { stage: record2.stage } : {},
5324
+ id: record3.id,
5325
+ type: record3.type,
5326
+ name: record3.name,
5327
+ ...record3.stage ? { stage: record3.stage } : {},
4664
5328
  fields,
4665
- createdAt: record2.createdAt,
4666
- updatedAt: record2.updatedAt
5329
+ createdAt: record3.createdAt,
5330
+ updatedAt: record3.updatedAt
4667
5331
  };
4668
5332
  }
4669
5333
  var handleNetworkRecords = async (req, url, env, ctx) => {
@@ -4672,15 +5336,15 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
4672
5336
  const secret = await getVaultSecret(db, "network_share_secret");
4673
5337
  const readers = ctx.chapter.network.readers;
4674
5338
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
4675
- const verified = await (0, import_db7.verifyFederatedRequest)(req, {
5339
+ const verified = await (0, import_db8.verifyFederatedRequest)(req, {
4676
5340
  secret,
4677
5341
  senders: readers.map((reader2) => reader2.id)
4678
5342
  });
4679
5343
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
4680
5344
  const reader = readers.find((candidate) => candidate.id === verified.sender);
4681
5345
  const type = url.searchParams.get("type") ?? "";
4682
- const allowed = reader?.fields[type];
4683
- if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
5346
+ const allowed2 = reader?.fields[type];
5347
+ if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
4684
5348
  const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
4685
5349
  const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
4686
5350
  const search = (url.searchParams.get("search") ?? "").trim();
@@ -4701,7 +5365,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
4701
5365
  version: 1,
4702
5366
  site: { id: ctx.chapter.id, name: ctx.chapter.name },
4703
5367
  type,
4704
- records: page.records.map((record2) => projectRecord(record2, allowed)),
5368
+ records: page.records.map((record3) => projectRecord(record3, allowed2)),
4705
5369
  total: page.total,
4706
5370
  limit: page.limit,
4707
5371
  offset: page.offset
@@ -4717,7 +5381,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
4717
5381
  const destination = new URL("/api/network/records", target.url);
4718
5382
  destination.search = query.toString();
4719
5383
  try {
4720
- const headers = await (0, import_db7.signFederatedRequest)({
5384
+ const headers = await (0, import_db8.signFederatedRequest)({
4721
5385
  secret,
4722
5386
  sender: ctx.chapter.id,
4723
5387
  method: "GET",
@@ -4852,7 +5516,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
4852
5516
 
4853
5517
  // src/worker-routes-network-shared-notes.ts
4854
5518
  var import_crm8 = require("@odla-ai/crm");
4855
- var import_db8 = require("@odla-ai/db");
5519
+ var import_db9 = require("@odla-ai/db");
4856
5520
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
4857
5521
  var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
4858
5522
  var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
@@ -4911,7 +5575,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
4911
5575
  const secret = await getVaultSecret(db, "network_share_secret");
4912
5576
  const readers = ctx.chapter.network.readers;
4913
5577
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
4914
- const verified = await (0, import_db8.verifyFederatedRequest)(req, {
5578
+ const verified = await (0, import_db9.verifyFederatedRequest)(req, {
4915
5579
  secret,
4916
5580
  senders: readers.map((reader2) => reader2.id)
4917
5581
  });
@@ -4922,8 +5586,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
4922
5586
  if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
4923
5587
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
4924
5588
  }
4925
- const record2 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
4926
- if (!record2 || record2.type !== recordType) return json({ error: "record not found" }, 404);
5589
+ const record3 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
5590
+ if (!record3 || record3.type !== recordType) return json({ error: "record not found" }, 404);
4927
5591
  if (req.method === "GET") {
4928
5592
  return json({
4929
5593
  version: 1,
@@ -5008,7 +5672,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5008
5672
  const destination = new URL("/api/network/shared-notes", target.url);
5009
5673
  destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
5010
5674
  try {
5011
- const headers = await (0, import_db8.signFederatedRequest)({
5675
+ const headers = await (0, import_db9.signFederatedRequest)({
5012
5676
  secret,
5013
5677
  sender: ctx.chapter.id,
5014
5678
  method: req.method,
@@ -5036,31 +5700,17 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5036
5700
 
5037
5701
  // src/worker-routes-formation.ts
5038
5702
  var import_crm9 = require("@odla-ai/crm");
5039
-
5040
- // src/formation.ts
5041
- function formationFields(crm, formation) {
5042
- const def = crm.type(formation.type);
5043
- return [...formation.required, ...formation.optional].map((id) => {
5044
- const field = def.fields[id];
5045
- return {
5046
- id,
5047
- label: field.label ?? id,
5048
- type: field.type,
5049
- required: formation.required.includes(id),
5050
- ...field.options ? { options: field.options } : {}
5051
- };
5052
- });
5053
- }
5054
-
5055
- // src/worker-routes-formation.ts
5056
5703
  var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
5057
5704
  var handleFormation = async (req, url, env, ctx) => {
5058
5705
  const formation = ctx.chapter.formation;
5059
5706
  if (!formation.enabled) return null;
5060
5707
  if (req.method === "GET" && url.pathname === "/api/formation/config") {
5708
+ const fieldContract = chapterFlowFieldContract(ctx.chapter, "formation");
5061
5709
  return json({
5062
5710
  type: formation.type,
5063
- fields: formationFields(ctx.chapter.crm, formation)
5711
+ fieldContract,
5712
+ // Backward compatibility for hosts that consumed the original array.
5713
+ fields: fieldContract.fields
5064
5714
  });
5065
5715
  }
5066
5716
  if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
@@ -5077,15 +5727,15 @@ var handleFormation = async (req, url, env, ctx) => {
5077
5727
  if (submissionId && !SUBMISSION.test(submissionId)) {
5078
5728
  return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
5079
5729
  }
5080
- const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
5081
- const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
5730
+ const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
5731
+ const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed2.has(field));
5082
5732
  if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
5083
5733
  for (const field of formation.required) {
5084
5734
  const value = body[field];
5085
5735
  if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
5086
5736
  }
5087
5737
  const input = {};
5088
- for (const field of allowed) {
5738
+ for (const field of allowed2) {
5089
5739
  const value = body[field];
5090
5740
  if (value === void 0) continue;
5091
5741
  const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
@@ -5129,11 +5779,11 @@ var handleFormation = async (req, url, env, ctx) => {
5129
5779
  };
5130
5780
 
5131
5781
  // src/signup-control-delivery.ts
5132
- var import_db9 = require("@odla-ai/db");
5782
+ var import_db10 = require("@odla-ai/db");
5133
5783
  async function deliverSignupControl(options) {
5134
5784
  const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
5135
5785
  const body = JSON.stringify(options.envelope);
5136
- const signed = await (0, import_db9.signFederatedRequest)({
5786
+ const signed = await (0, import_db10.signFederatedRequest)({
5137
5787
  secret: options.secret,
5138
5788
  sender: options.sender,
5139
5789
  method: "POST",
@@ -5153,12 +5803,37 @@ async function deliverSignupControl(options) {
5153
5803
  return result;
5154
5804
  }
5155
5805
 
5806
+ // src/member-content-delivery.ts
5807
+ var import_db11 = require("@odla-ai/db");
5808
+ async function deliverMemberContent(options) {
5809
+ const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
5810
+ const body = JSON.stringify(options.envelope);
5811
+ const signed = await (0, import_db11.signFederatedRequest)({
5812
+ secret: options.secret,
5813
+ sender: options.sender,
5814
+ method: "POST",
5815
+ url: destination,
5816
+ body
5817
+ });
5818
+ const response = await (options.fetch ?? globalThis.fetch)(destination, {
5819
+ method: "POST",
5820
+ headers: { ...signed, "content-type": "application/json" },
5821
+ body
5822
+ });
5823
+ const result = await response.json();
5824
+ if (!response.ok || !("ok" in result) || result.ok !== true) {
5825
+ const failure = result;
5826
+ throw new Error(`member content rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
5827
+ }
5828
+ return result;
5829
+ }
5830
+
5156
5831
  // src/admission-grants-delivery.ts
5157
- var import_db10 = require("@odla-ai/db");
5832
+ var import_db12 = require("@odla-ai/db");
5158
5833
  async function deliverAdmissionGrants(options) {
5159
5834
  const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
5160
5835
  const body = JSON.stringify(options.envelope);
5161
- const signed = await (0, import_db10.signFederatedRequest)({
5836
+ const signed = await (0, import_db12.signFederatedRequest)({
5162
5837
  secret: options.secret,
5163
5838
  sender: options.sender,
5164
5839
  method: "POST",
@@ -5317,12 +5992,14 @@ var BUILTIN_ROUTES = [
5317
5992
  handleCrm,
5318
5993
  handleNetworkShared,
5319
5994
  handleSignupControl,
5995
+ handleMemberContentControl,
5320
5996
  handleAdmissionGrants,
5321
5997
  handleNetworkSnapshot,
5322
5998
  handleNetworkRecords,
5323
5999
  handleNetworkSharedNotes,
5324
6000
  handleFormation,
5325
6001
  handleMember,
6002
+ handleMemberContent,
5326
6003
  handleGifts,
5327
6004
  handleSchedule,
5328
6005
  handlePayments,
@@ -5338,6 +6015,7 @@ var BUILTIN_ROUTES = [
5338
6015
  // Aggregation
5339
6016
  handleAdminDashboard,
5340
6017
  handleAdminBilling,
6018
+ handleAdminMemberContent,
5341
6019
  // Lifecycle actions
5342
6020
  handleAdminMeetingReschedule,
5343
6021
  handleAdminMeetingCancel,