@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.
@@ -167,204 +167,10 @@ async function applicationIdForSubmission(submissionId) {
167
167
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
168
168
  }
169
169
 
170
- // src/admission-grants-contract.ts
171
- var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
172
- var ADMISSION_GRANTS_PATH = "/api/admission-grants";
173
-
174
- // src/signup-control-canonical.ts
175
- var encoder = new TextEncoder();
176
- function canonicalValue(value) {
177
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
178
- return JSON.stringify(value);
179
- }
180
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
181
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
182
- const record2 = value;
183
- const entries = Object.keys(record2).sort().flatMap((key) => {
184
- const item = record2[key];
185
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
186
- });
187
- return `{${entries.join(",")}}`;
188
- }
189
- function canonicalSignupControl(value) {
190
- return canonicalValue(value);
191
- }
192
- async function signupControlDigest(document) {
193
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
194
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
195
- }
196
-
197
- // src/admission-grants.ts
198
- var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
199
- var MAX_GRANTS = 2e3;
200
- var MAX_NOTE = 2e3;
201
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
202
- var isAdmissionEmail = (value) => EMAIL_RE.test(value);
203
- function normalizeAdmissionEmail(email) {
170
+ // src/email-identity.ts
171
+ function normalizeChapterEmail(email) {
204
172
  return email.trim().toLowerCase();
205
173
  }
206
- function validateAdmissionGrantDocument(input) {
207
- const errors = [];
208
- const root = input && typeof input === "object" ? input : {};
209
- if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
210
- const revision = Number(root.revision);
211
- const generatedAt = Number(root.generatedAt);
212
- if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
213
- if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
214
- const source = root.source && typeof root.source === "object" ? root.source : {};
215
- const target = root.target && typeof root.target === "object" ? root.target : {};
216
- const sourceId = typeof source.appId === "string" ? source.appId : "";
217
- const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
218
- const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
219
- const environment = typeof target.environment === "string" ? target.environment : "";
220
- if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
221
- if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
222
- if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
223
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
224
- if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
225
- const rawGrants = Array.isArray(root.grants) ? root.grants : [];
226
- if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
227
- const grants = [];
228
- const seenIds = /* @__PURE__ */ new Set();
229
- const seenEmails = /* @__PURE__ */ new Set();
230
- for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
231
- const grant = raw && typeof raw === "object" ? raw : {};
232
- const id = typeof grant.id === "string" ? grant.id : "";
233
- const email = typeof grant.email === "string" ? grant.email.trim() : "";
234
- const normalized = normalizeAdmissionEmail(email);
235
- const decision = grant.decision;
236
- const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
237
- const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
238
- if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
239
- if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
240
- if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
241
- if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
242
- if (decision !== "waive_interview" && decision !== "require_interview") {
243
- errors.push(`document.grants[${index}].decision is invalid`);
244
- }
245
- if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
246
- if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
247
- errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
248
- }
249
- seenIds.add(id);
250
- seenEmails.add(normalized);
251
- grants.push({
252
- id,
253
- email,
254
- decision,
255
- ...note ? { note } : {},
256
- ...expiresAt !== void 0 ? { expiresAt } : {}
257
- });
258
- }
259
- if (errors.length) return { ok: false, errors };
260
- return {
261
- ok: true,
262
- value: {
263
- schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
264
- revision,
265
- generatedAt,
266
- source: { appId: sourceId, actor: sourceActor },
267
- target: { chapterId, environment },
268
- grants
269
- }
270
- };
271
- }
272
- var rows = (value) => Array.isArray(value) ? value : [];
273
- function asGrant(row) {
274
- if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
275
- if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
276
- if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
277
- return row;
278
- }
279
- async function findAdmissionGrant(db, targetId, email, now) {
280
- const emailNormalized = normalizeAdmissionEmail(email);
281
- if (!isAdmissionEmail(emailNormalized)) return null;
282
- const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
283
- const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
284
- grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
285
- )).sort((a, b) => {
286
- const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
287
- return scope || b.createdAt - a.createdAt;
288
- });
289
- return active[0] ?? null;
290
- }
291
- async function hasAdmissionRevision(db, sourceId, targetId, environment) {
292
- const where = { sourceId, targetId, environment };
293
- if (db.aggregate) {
294
- return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
295
- }
296
- const found = await db.query({ admissionGrantRevisions: {} });
297
- return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
298
- }
299
- async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
300
- if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
301
- const result = await db.query({ admissionGrantRevisions: { $: {
302
- where: { sourceId, targetId, environment },
303
- order: { revision: "desc" },
304
- limit: 1
305
- } } });
306
- const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
307
- return row ? row : null;
308
- }
309
- async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
310
- const { document, digest } = envelope;
311
- const current = rows((await db.query({ admissionGrants: { $: {
312
- where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
313
- limit: MAX_GRANTS
314
- } } })).admissionGrants);
315
- const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
316
- const ops = [];
317
- for (const row of current) {
318
- const id = String(row.id ?? "");
319
- if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
320
- active: false,
321
- revokedAt: receivedAt,
322
- revokedBy: document.source.actor,
323
- updatedAt: receivedAt,
324
- updatedBy: document.source.actor
325
- } });
326
- }
327
- for (const grant of document.grants) {
328
- const id = `admission:parent:${document.source.appId}:${grant.id}`;
329
- const existing = current.find((row) => row.id === id);
330
- ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
331
- id,
332
- sourceGrantId: grant.id,
333
- sourceScope: "parent",
334
- sourceId: document.source.appId,
335
- targetId: document.target.chapterId,
336
- email: grant.email,
337
- emailNormalized: normalizeAdmissionEmail(grant.email),
338
- decision: grant.decision,
339
- ...grant.note ? { note: grant.note } : {},
340
- ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
341
- active: true,
342
- sourceRevision: document.revision,
343
- createdAt: Number(existing?.createdAt ?? receivedAt),
344
- createdBy: String(existing?.createdBy ?? document.source.actor),
345
- updatedAt: receivedAt,
346
- updatedBy: document.source.actor
347
- } });
348
- const retract = [
349
- ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
350
- ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
351
- ];
352
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
353
- }
354
- if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
355
- const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
356
- ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
357
- id: revisionId,
358
- sourceId: document.source.appId,
359
- targetId: document.target.chapterId,
360
- environment: document.target.environment,
361
- revision: document.revision,
362
- digest,
363
- active: true,
364
- receivedAt
365
- } });
366
- await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
367
- }
368
174
 
369
175
  // src/member.ts
370
176
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
@@ -417,9 +223,9 @@ function resolveApplication(a) {
417
223
  validateEmail: a?.validateEmail ?? true
418
224
  };
419
225
  }
420
- var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
226
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
421
227
  function isValidEmail(value) {
422
- return typeof value === "string" && EMAIL_RE2.test(value);
228
+ return typeof value === "string" && EMAIL_RE.test(value);
423
229
  }
424
230
  function clampArray(value, max) {
425
231
  if (!Array.isArray(value)) return value;
@@ -431,14 +237,14 @@ function hasDisclaimerAck(fields) {
431
237
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
432
238
  function applicantProfile(chapter, fields) {
433
239
  const app = chapter.application;
434
- const allowed = (f) => app.profileFields.includes(f);
240
+ const allowed2 = (f) => app.profileFields.includes(f);
435
241
  const profile = {};
436
242
  for (const f of [...app.required, ...app.optional]) {
437
- if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
243
+ if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
438
244
  const v = fields[f];
439
245
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
440
246
  }
441
- if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
247
+ if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
442
248
  return Object.keys(profile).length > 0 ? profile : void 0;
443
249
  }
444
250
  async function submitApplication(db, chapter, fields, opts) {
@@ -454,7 +260,7 @@ async function submitApplication(db, chapter, fields, opts) {
454
260
  const cap = app.maxLen[f] ?? app.defaultMaxLen;
455
261
  if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
456
262
  }
457
- if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
263
+ if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
458
264
  return { ok: false, error: "email must be a valid email address" };
459
265
  }
460
266
  const acked = hasDisclaimerAck(fields);
@@ -470,7 +276,7 @@ async function submitApplication(db, chapter, fields, opts) {
470
276
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
471
277
  if (opts.groupId) row.groupId = opts.groupId;
472
278
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
473
- row.emailNormalized = normalizeAdmissionEmail(fields.email);
279
+ row.emailNormalized = normalizeChapterEmail(fields.email);
474
280
  }
475
281
  if (opts.admissionGrant?.decision === "waive_interview") {
476
282
  row.admissionGrantId = opts.admissionGrant.id;
@@ -687,14 +493,289 @@ function meetingCreateRow(i) {
687
493
  createdAt: i.createdAt
688
494
  };
689
495
  }
690
- function meetingRescheduleUpdate(startAt, endAt) {
691
- return { startAt, endAt, drift: "none" };
496
+ function meetingRescheduleUpdate(startAt, endAt) {
497
+ return { startAt, endAt, drift: "none" };
498
+ }
499
+ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
500
+ return {
501
+ meetingAt: startAt,
502
+ ...htmlLink ? { meetingLink: htmlLink } : {},
503
+ ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
504
+ };
505
+ }
506
+
507
+ // src/admission-grants-contract.ts
508
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
509
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
510
+
511
+ // src/signup-control-canonical.ts
512
+ var encoder = new TextEncoder();
513
+ function canonicalValue(value) {
514
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
515
+ return JSON.stringify(value);
516
+ }
517
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
518
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
519
+ const record3 = value;
520
+ const entries = Object.keys(record3).sort().flatMap((key) => {
521
+ const item = record3[key];
522
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
523
+ });
524
+ return `{${entries.join(",")}}`;
525
+ }
526
+ function canonicalSignupControl(value) {
527
+ return canonicalValue(value);
528
+ }
529
+ async function signupControlDigest(document) {
530
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
531
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
532
+ }
533
+
534
+ // src/admission-grants.ts
535
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
536
+ var MAX_GRANTS = 2e3;
537
+ var MAX_NOTE = 2e3;
538
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
539
+ var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
540
+ function normalizeAdmissionEmail(email) {
541
+ return normalizeChapterEmail(email);
542
+ }
543
+ function validateAdmissionGrantDocument(input) {
544
+ const errors = [];
545
+ const root = input && typeof input === "object" ? input : {};
546
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
547
+ const revision = Number(root.revision);
548
+ const generatedAt = Number(root.generatedAt);
549
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
550
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
551
+ const source = root.source && typeof root.source === "object" ? root.source : {};
552
+ const target = root.target && typeof root.target === "object" ? root.target : {};
553
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
554
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
555
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
556
+ const environment = typeof target.environment === "string" ? target.environment : "";
557
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
558
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
559
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
560
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
561
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
562
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
563
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
564
+ const grants = [];
565
+ const seenIds = /* @__PURE__ */ new Set();
566
+ const seenEmails = /* @__PURE__ */ new Set();
567
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
568
+ const grant = raw && typeof raw === "object" ? raw : {};
569
+ const id = typeof grant.id === "string" ? grant.id : "";
570
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
571
+ const normalized = normalizeAdmissionEmail(email);
572
+ const decision = grant.decision;
573
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
574
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
575
+ if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
576
+ if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
577
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
578
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
579
+ if (decision !== "waive_interview" && decision !== "require_interview") {
580
+ errors.push(`document.grants[${index}].decision is invalid`);
581
+ }
582
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
583
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
584
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
585
+ }
586
+ seenIds.add(id);
587
+ seenEmails.add(normalized);
588
+ grants.push({
589
+ id,
590
+ email,
591
+ decision,
592
+ ...note ? { note } : {},
593
+ ...expiresAt !== void 0 ? { expiresAt } : {}
594
+ });
595
+ }
596
+ if (errors.length) return { ok: false, errors };
597
+ return {
598
+ ok: true,
599
+ value: {
600
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
601
+ revision,
602
+ generatedAt,
603
+ source: { appId: sourceId, actor: sourceActor },
604
+ target: { chapterId, environment },
605
+ grants
606
+ }
607
+ };
608
+ }
609
+ var rows = (value) => Array.isArray(value) ? value : [];
610
+ function asGrant(row) {
611
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
612
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
613
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
614
+ return row;
615
+ }
616
+ async function findAdmissionGrant(db, targetId, email, now) {
617
+ const emailNormalized = normalizeAdmissionEmail(email);
618
+ if (!isAdmissionEmail(emailNormalized)) return null;
619
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
620
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
621
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
622
+ )).sort((a, b) => {
623
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
624
+ return scope || b.createdAt - a.createdAt;
625
+ });
626
+ return active[0] ?? null;
627
+ }
628
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
629
+ const where = { sourceId, targetId, environment };
630
+ if (db.aggregate) {
631
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
632
+ }
633
+ const found = await db.query({ admissionGrantRevisions: {} });
634
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
635
+ }
636
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
637
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
638
+ const result = await db.query({ admissionGrantRevisions: { $: {
639
+ where: { sourceId, targetId, environment },
640
+ order: { revision: "desc" },
641
+ limit: 1
642
+ } } });
643
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
644
+ return row ? row : null;
645
+ }
646
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
647
+ const { document, digest } = envelope;
648
+ const current = rows((await db.query({ admissionGrants: { $: {
649
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
650
+ limit: MAX_GRANTS
651
+ } } })).admissionGrants);
652
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
653
+ const ops = [];
654
+ for (const row of current) {
655
+ const id = String(row.id ?? "");
656
+ if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
657
+ active: false,
658
+ revokedAt: receivedAt,
659
+ revokedBy: document.source.actor,
660
+ updatedAt: receivedAt,
661
+ updatedBy: document.source.actor
662
+ } });
663
+ }
664
+ for (const grant of document.grants) {
665
+ const id = `admission:parent:${document.source.appId}:${grant.id}`;
666
+ const existing = current.find((row) => row.id === id);
667
+ ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
668
+ id,
669
+ sourceGrantId: grant.id,
670
+ sourceScope: "parent",
671
+ sourceId: document.source.appId,
672
+ targetId: document.target.chapterId,
673
+ email: grant.email,
674
+ emailNormalized: normalizeAdmissionEmail(grant.email),
675
+ decision: grant.decision,
676
+ ...grant.note ? { note: grant.note } : {},
677
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
678
+ active: true,
679
+ sourceRevision: document.revision,
680
+ createdAt: Number(existing?.createdAt ?? receivedAt),
681
+ createdBy: String(existing?.createdBy ?? document.source.actor),
682
+ updatedAt: receivedAt,
683
+ updatedBy: document.source.actor
684
+ } });
685
+ const retract = [
686
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
687
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
688
+ ];
689
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
690
+ }
691
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
692
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
693
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
694
+ id: revisionId,
695
+ sourceId: document.source.appId,
696
+ targetId: document.target.chapterId,
697
+ environment: document.target.environment,
698
+ revision: document.revision,
699
+ digest,
700
+ active: true,
701
+ receivedAt
702
+ } });
703
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
704
+ }
705
+
706
+ // src/flow-fields.ts
707
+ var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
708
+ var APPLICATION_LABELS = {
709
+ firstName: "First name",
710
+ lastName: "Last name",
711
+ email: "Email",
712
+ referral: "Referral source",
713
+ referralName: "Referral name",
714
+ whoYouAre: "Who you are",
715
+ linkedin: "LinkedIn",
716
+ message: "Intro message",
717
+ phone: "Phone",
718
+ state: "State"
719
+ };
720
+ function humanize(id) {
721
+ const text3 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
722
+ return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id;
723
+ }
724
+ function applicationDefs(chapter) {
725
+ try {
726
+ return chapter.crm.type("person").fields;
727
+ } catch {
728
+ return {};
729
+ }
692
730
  }
693
- function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
731
+ function applicationType(id, def) {
732
+ if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
733
+ return id === "email" ? "email" : id === "linkedin" ? "url" : "string";
734
+ }
735
+ function applicationFlowFields(chapter, application = chapter.application) {
736
+ const defs = applicationDefs(chapter);
737
+ return [...application.required, ...application.optional].map((id, order) => {
738
+ const def = defs[id];
739
+ const conditions = application.conditions[id];
740
+ const type = applicationType(id, def);
741
+ return {
742
+ id,
743
+ label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
744
+ type,
745
+ required: application.required.includes(id),
746
+ order,
747
+ maxLength: application.maxLen[id] ?? application.defaultMaxLen,
748
+ ...type === "enum" && def?.options ? { options: [...def.options] } : {},
749
+ ...conditions ? { conditions: { ...conditions } } : {},
750
+ ...type === "email" ? { emailIdentity: "literal" } : {}
751
+ };
752
+ });
753
+ }
754
+ function resolveFormationFlowFields(crm, formation) {
755
+ if (!formation.enabled) return [];
756
+ const def = crm.type(formation.type);
757
+ return [...formation.required, ...formation.optional].map((id, order) => {
758
+ const field = def.fields[id];
759
+ return {
760
+ id,
761
+ label: field.label ?? humanize(id),
762
+ type: field.type,
763
+ required: formation.required.includes(id),
764
+ order,
765
+ maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
766
+ ...field.options ? { options: [...field.options] } : {},
767
+ ...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
768
+ };
769
+ });
770
+ }
771
+ function formationFlowFields(chapter, formation = chapter.formation) {
772
+ return resolveFormationFlowFields(chapter.crm, formation);
773
+ }
774
+ function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
694
775
  return {
695
- meetingAt: startAt,
696
- ...htmlLink ? { meetingLink: htmlLink } : {},
697
- ...currentStatus !== "call_scheduled" ? { status: "call_scheduled" } : {}
776
+ schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
777
+ flow,
778
+ fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
698
779
  };
699
780
  }
700
781
 
@@ -731,8 +812,8 @@ function validateGiftOfferCatalog(input, tierIds) {
731
812
  errors.push(`${path} must be an object`);
732
813
  continue;
733
814
  }
734
- const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
735
- for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
815
+ const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
816
+ for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
736
817
  const id = text(item.id, `${path}.id`, errors, 80);
737
818
  if (id && !ID2.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
738
819
  if (ids.has(id)) errors.push(`${path}.id duplicates "${id}"`);
@@ -829,8 +910,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
829
910
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
830
911
  const page = Array.isArray(data) ? data : [];
831
912
  for (const u of page) {
832
- const record2 = toRecord(u);
833
- if (record2) out.push(record2);
913
+ const record3 = toRecord(u);
914
+ if (record3) out.push(record3);
834
915
  }
835
916
  if (page.length < PAGE) break;
836
917
  }
@@ -892,34 +973,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
892
973
  if (readable && raw.length <= 64) return raw;
893
974
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
894
975
  }
895
- function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
896
- if ("input" in record2 && record2.version === 2) {
976
+ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
977
+ if ("input" in record3 && record3.version === 2) {
897
978
  return {
898
979
  version: 2,
899
- sourceId: record2.source.siteId,
900
- sourceRecordId: record2.source.recordId,
901
- type: record2.type,
902
- input: record2.input
980
+ sourceId: record3.source.siteId,
981
+ sourceRecordId: record3.source.recordId,
982
+ type: record3.type,
983
+ input: record3.input
903
984
  };
904
985
  }
905
- if ("input" in record2) {
986
+ if ("input" in record3) {
906
987
  return {
907
988
  version: 1,
908
989
  sourceId: fallbackSourceId,
909
- sourceRecordId: record2.hubRecordId,
910
- type: record2.type,
911
- input: record2.input
990
+ sourceRecordId: record3.hubRecordId,
991
+ type: record3.type,
992
+ input: record3.input
912
993
  };
913
994
  }
914
- if ("type" in record2 && record2.type === "company") {
915
- const input = { name: record2.name };
995
+ if ("type" in record3 && record3.type === "company") {
996
+ const input = { name: record3.name };
916
997
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
917
- if (record2[key]) input[key] = record2[key];
998
+ if (record3[key]) input[key] = record3[key];
918
999
  }
919
1000
  return {
920
1001
  version: 1,
921
1002
  sourceId: fallbackSourceId,
922
- sourceRecordId: record2.hubRecordId,
1003
+ sourceRecordId: record3.hubRecordId,
923
1004
  type: "company",
924
1005
  input
925
1006
  };
@@ -927,28 +1008,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
927
1008
  return {
928
1009
  version: 1,
929
1010
  sourceId: fallbackSourceId,
930
- sourceRecordId: record2.hubRecordId,
1011
+ sourceRecordId: record3.hubRecordId,
931
1012
  type: "person",
932
- input: sharedPersonInput(record2)
1013
+ input: sharedPersonInput(record3)
933
1014
  };
934
1015
  }
935
- function sharedRecordFromCrm(crm, record2, target, sourceId) {
936
- if (target.fields && !target.fields[record2.type]) {
937
- throw new Error(`${target.name} does not accept "${record2.type}" records`);
1016
+ function sharedRecordFromCrm(crm, record3, target, sourceId) {
1017
+ if (target.fields && !target.fields[record3.type]) {
1018
+ throw new Error(`${target.name} does not accept "${record3.type}" records`);
938
1019
  }
939
- const def = crm.type(record2.type);
940
- const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
1020
+ const def = crm.type(record3.type);
1021
+ const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
941
1022
  if (!fields) {
942
- throw new Error(`${target.name} requires an explicit field allowlist for "${record2.type}" records`);
1023
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
943
1024
  }
944
1025
  const nameField = def.nameField ?? "name";
945
1026
  const input = {};
946
1027
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
947
- const value = record2.fields?.[field];
1028
+ const value = record3.fields?.[field];
948
1029
  if (value !== void 0) input[field] = value;
949
1030
  }
950
- if (input[nameField] === void 0) input[nameField] = record2.name;
951
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
1031
+ if (input[nameField] === void 0) input[nameField] = record3.name;
1032
+ return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
952
1033
  }
953
1034
  async function upsertPerson(deps, opts) {
954
1035
  const email = opts.email.toLowerCase();
@@ -962,11 +1043,11 @@ async function upsertPerson(deps, opts) {
962
1043
  const created = await createRecord(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
963
1044
  return { recordId: created.id };
964
1045
  }
965
- async function findSharedRecord(deps, record2, tag) {
1046
+ async function findSharedRecord(deps, record3, tag) {
966
1047
  const structured = await getRecordByOrigin(
967
1048
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
968
- record2.sourceId,
969
- record2.sourceRecordId
1049
+ record3.sourceId,
1050
+ record3.sourceRecordId
970
1051
  );
971
1052
  if (structured) return structured;
972
1053
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -975,53 +1056,53 @@ async function findSharedRecord(deps, record2, tag) {
975
1056
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
976
1057
  if (found.crm_record?.[0]) return found.crm_record[0];
977
1058
  }
978
- const def = deps.crm.type(record2.type);
1059
+ const def = deps.crm.type(record3.type);
979
1060
  const emailField = def.emailField;
980
- if (emailField && typeof record2.input[emailField] === "string") {
981
- const primaryEmail = record2.input[emailField].toLowerCase();
1061
+ if (emailField && typeof record3.input[emailField] === "string") {
1062
+ const primaryEmail = record3.input[emailField].toLowerCase();
982
1063
  const found = await deps.db.query({
983
- crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
1064
+ crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
984
1065
  });
985
1066
  if (found.crm_record?.[0]) return found.crm_record[0];
986
1067
  }
987
- const domain = record2.input.domain;
1068
+ const domain = record3.input.domain;
988
1069
  const domainSlot = def.fields.domain?.slot;
989
1070
  if (typeof domain === "string" && domainSlot) {
990
1071
  const found = await deps.db.query({
991
- crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
1072
+ crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
992
1073
  });
993
1074
  if (found.crm_record?.[0]) return found.crm_record[0];
994
1075
  }
995
1076
  const nameField = def.nameField ?? "name";
996
- const name = record2.input[nameField];
997
- if (record2.type === "company" && typeof name === "string" && name.trim()) {
1077
+ const name = record3.input[nameField];
1078
+ if (record3.type === "company" && typeof name === "string" && name.trim()) {
998
1079
  const found = await deps.db.query({
999
- crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
1080
+ crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
1000
1081
  });
1001
1082
  if (found.crm_record?.[0]) return found.crm_record[0];
1002
1083
  }
1003
1084
  return void 0;
1004
1085
  }
1005
1086
  async function projectSharedRecord(deps, shared, options = {}) {
1006
- const record2 = normalizeSharedRecord(shared, options.sourceId);
1007
- if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
1087
+ const record3 = normalizeSharedRecord(shared, options.sourceId);
1088
+ if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
1008
1089
  throw new Error("type, source site id, and source record id must be non-empty");
1009
1090
  }
1010
- if (options.sourceId && options.sourceId !== record2.sourceId) {
1091
+ if (options.sourceId && options.sourceId !== record3.sourceId) {
1011
1092
  throw new Error("signed sender does not match payload source");
1012
1093
  }
1013
- const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
1094
+ const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
1014
1095
  const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1015
- const existing = await findSharedRecord(deps, record2, tag);
1096
+ const existing = await findSharedRecord(deps, record3, tag);
1016
1097
  let recordId;
1017
1098
  if (existing && typeof existing.id === "string") {
1018
- await updateRecord(crmDeps3, { id: existing.id, input: record2.input });
1099
+ await updateRecord(crmDeps3, { id: existing.id, input: record3.input });
1019
1100
  recordId = existing.id;
1020
1101
  } else {
1021
- recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
1102
+ recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
1022
1103
  await createRecord({ ...crmDeps3, newId: () => recordId }, {
1023
- type: record2.type,
1024
- input: record2.input,
1104
+ type: record3.type,
1105
+ input: record3.input,
1025
1106
  mutationId: `share-create:${tag}`
1026
1107
  });
1027
1108
  }
@@ -1031,10 +1112,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
1031
1112
  );
1032
1113
  await upsertRecordOrigin(crmDeps3, {
1033
1114
  recordId,
1034
- sourceId: record2.sourceId,
1035
- sourceRecordId: record2.sourceRecordId,
1115
+ sourceId: record3.sourceId,
1116
+ sourceRecordId: record3.sourceRecordId,
1036
1117
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
1037
- payloadVersion: record2.version
1118
+ payloadVersion: record3.version
1038
1119
  });
1039
1120
  return { recordId };
1040
1121
  }
@@ -1483,7 +1564,8 @@ function applicationSummary(app) {
1483
1564
  meetingLink: app.meetingLink ?? null,
1484
1565
  paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
1485
1566
  renewalAt: app.renewalAt ?? null,
1486
- canceled: app.canceled === true
1567
+ canceled: app.canceled === true,
1568
+ tierId: app.tierId ?? null
1487
1569
  };
1488
1570
  }
1489
1571
  function memberApplication(app, meeting, defaultTimezone) {
@@ -1707,7 +1789,7 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
1707
1789
  // src/worker-routes.ts
1708
1790
  async function memberSessionApplication(db, chapterId, email) {
1709
1791
  let apps = (await db.query({ applications: { $: {
1710
- where: { emailNormalized: normalizeAdmissionEmail(email) },
1792
+ where: { emailNormalized: normalizeChapterEmail(email) },
1711
1793
  limit: 20
1712
1794
  } } })).applications;
1713
1795
  if (!apps?.length) apps = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
@@ -1806,6 +1888,7 @@ var handleMember = async (req, url, env, ctx) => {
1806
1888
  const application = controlledApplication(chapter.application, activeControl?.document);
1807
1889
  return json({
1808
1890
  ...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
1891
+ fieldContract: chapterFlowFieldContract(chapter, "join", application),
1809
1892
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
1810
1893
  ...activeControl ? { signupControl: {
1811
1894
  revision: activeControl.revision,
@@ -1957,8 +2040,8 @@ var handleNetworkShared = async (req, url, env, ctx) => {
1957
2040
  payload,
1958
2041
  sourceId ? { sourceId } : {}
1959
2042
  );
1960
- const record2 = normalizeSharedRecord(payload, sourceId);
1961
- return json({ recordId, type: record2.type });
2043
+ const record3 = normalizeSharedRecord(payload, sourceId);
2044
+ return json({ recordId, type: record3.type });
1962
2045
  } catch (error) {
1963
2046
  return json({
1964
2047
  error: error instanceof Error ? error.message : "invalid shared record"
@@ -1998,9 +2081,9 @@ var MAX_BENEFITS = 24;
1998
2081
  var MAX_MARKDOWN = 65536;
1999
2082
  var MAX_LABEL = 160;
2000
2083
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2001
- function rejectUnknownKeys(value, allowed, path, errors) {
2084
+ function rejectUnknownKeys(value, allowed2, path, errors) {
2002
2085
  for (const key of Object.keys(value)) {
2003
- if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2086
+ if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2004
2087
  }
2005
2088
  }
2006
2089
  function boundedString(value, path, errors, max, required = false) {
@@ -2156,8 +2239,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2156
2239
  ]);
2157
2240
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2158
2241
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2159
- function unknownKeys(value, allowed, path, errors) {
2160
- for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2242
+ function unknownKeys(value, allowed2, path, errors) {
2243
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2161
2244
  }
2162
2245
  function text2(value, path, errors, max = 65536) {
2163
2246
  if (typeof value !== "string" || !value.trim()) {
@@ -2242,10 +2325,10 @@ function policyOf(value, errors) {
2242
2325
  errors.push("document.policy must be an object");
2243
2326
  return void 0;
2244
2327
  }
2245
- const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2246
- unknownKeys(input, allowed, "document.policy", errors);
2328
+ const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2329
+ unknownKeys(input, allowed2, "document.policy", errors);
2247
2330
  const result = {};
2248
- for (const key of allowed) {
2331
+ for (const key of allowed2) {
2249
2332
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
2250
2333
  result[key] = text2(input[key], `document.policy.${key}`, errors);
2251
2334
  }
@@ -2384,9 +2467,589 @@ var handleSignupControl = async (req, url, env, ctx) => {
2384
2467
  });
2385
2468
  };
2386
2469
 
2387
- // src/worker-routes-admission-grants.ts
2470
+ // src/worker-routes-member-content.ts
2388
2471
  import { verifyFederatedRequest as verifyFederatedRequest3 } from "@odla-ai/db";
2472
+
2473
+ // src/member-content-canonical.ts
2474
+ var encoder2 = new TextEncoder();
2475
+ function canonicalValue2(value) {
2476
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2477
+ return JSON.stringify(value);
2478
+ }
2479
+ if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
2480
+ if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
2481
+ const record3 = value;
2482
+ const entries = Object.keys(record3).sort().flatMap((key) => {
2483
+ const item = record3[key];
2484
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
2485
+ });
2486
+ return `{${entries.join(",")}}`;
2487
+ }
2488
+ function canonicalMemberContent(value) {
2489
+ return canonicalValue2(value);
2490
+ }
2491
+ async function memberContentDigest(document) {
2492
+ const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
2493
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2494
+ }
2495
+
2496
+ // src/member-content-validation-helpers.ts
2497
+ var ID3 = /^[a-z][a-z0-9-]{0,79}$/;
2498
+ var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2499
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2500
+ var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
2501
+ var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
2502
+ var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
2503
+ var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
2504
+ var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
2505
+ var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
2506
+ var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
2507
+ var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
2508
+ var MAX_PAGES = 100;
2509
+ var MAX_MARKDOWN2 = 65536;
2510
+ var MAX_TEXT = 240;
2511
+ var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2512
+ function unknownKeys2(value, allowed2, path, errors) {
2513
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2514
+ }
2515
+ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
2516
+ if (value === void 0) {
2517
+ if (required) errors.push(`${path} is required`);
2518
+ return void 0;
2519
+ }
2520
+ if (typeof value !== "string") {
2521
+ errors.push(`${path} must be a string`);
2522
+ return void 0;
2523
+ }
2524
+ const valueText = value.trim();
2525
+ if (required && !valueText) errors.push(`${path} is required`);
2526
+ if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2527
+ return valueText;
2528
+ }
2529
+ function offerValue(value, path, errors, tiers, audience) {
2530
+ const raw = record2(value);
2531
+ if (!raw) {
2532
+ errors.push(`${path} must be an object`);
2533
+ return void 0;
2534
+ }
2535
+ unknownKeys2(raw, OFFER_KEYS, path, errors);
2536
+ const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2537
+ if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2538
+ if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2539
+ if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2540
+ errors.push(`${path}.tierId must grant access to this page`);
2541
+ }
2542
+ const label = textValue(raw.label, `${path}.label`, errors);
2543
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2);
2544
+ const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
2545
+ return {
2546
+ tierId,
2547
+ ...label ? { label } : {},
2548
+ ...bodyMarkdown ? { bodyMarkdown } : {},
2549
+ ...ctaLabel ? { ctaLabel } : {}
2550
+ };
2551
+ }
2552
+
2553
+ // src/member-content-validation.ts
2554
+ function validateMemberContentDocument(input, options = {}) {
2555
+ const errors = [];
2556
+ const root = record2(input);
2557
+ if (!root) return { ok: false, errors: ["document must be an object"] };
2558
+ unknownKeys2(root, ROOT_KEYS3, "document", errors);
2559
+ if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
2560
+ if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
2561
+ if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
2562
+ const sourceRaw = record2(root.source);
2563
+ let source = { type: "chapter", appId: "invalid" };
2564
+ if (!sourceRaw) errors.push("document.source must be an object");
2565
+ else {
2566
+ unknownKeys2(sourceRaw, SOURCE_KEYS, "document.source", errors);
2567
+ const type = sourceRaw.type;
2568
+ const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
2569
+ if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
2570
+ if (appId && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
2571
+ if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2572
+ if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
2573
+ if (type === "parent" || type === "chapter") source = { type, appId };
2574
+ }
2575
+ let target;
2576
+ if (root.target !== void 0) {
2577
+ const targetRaw = record2(root.target);
2578
+ if (!targetRaw) errors.push("document.target must be an object");
2579
+ else {
2580
+ unknownKeys2(targetRaw, TARGET_KEYS, "document.target", errors);
2581
+ const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
2582
+ const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
2583
+ target = { chapterId, environment };
2584
+ }
2585
+ }
2586
+ if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
2587
+ if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
2588
+ if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
2589
+ const rawPages = Array.isArray(root.pages) ? root.pages : [];
2590
+ if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
2591
+ const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
2592
+ const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
2593
+ const ids = /* @__PURE__ */ new Set();
2594
+ const slugs = /* @__PURE__ */ new Set();
2595
+ const pages = [];
2596
+ for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2597
+ const path = `document.pages[${index}]`;
2598
+ const raw = record2(value);
2599
+ if (!raw) {
2600
+ errors.push(`${path} must be an object`);
2601
+ continue;
2602
+ }
2603
+ unknownKeys2(raw, PAGE_KEYS, path, errors);
2604
+ const id = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
2605
+ const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
2606
+ const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2607
+ if (id && !ID3.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2608
+ if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2609
+ if (ids.has(id) || knownParentIds.has(id)) errors.push(`${path}.id duplicates page "${id}"`);
2610
+ if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
2611
+ ids.add(id);
2612
+ slugs.add(slug);
2613
+ const navRaw = record2(raw.navigation);
2614
+ let navigation = { section: "General", order: index };
2615
+ if (!navRaw) errors.push(`${path}.navigation must be an object`);
2616
+ else {
2617
+ unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
2618
+ const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
2619
+ const order = navRaw.order;
2620
+ if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
2621
+ errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
2622
+ }
2623
+ const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
2624
+ navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
2625
+ }
2626
+ const audienceRaw = record2(raw.audience);
2627
+ let audience = { kind: "all-members" };
2628
+ if (!audienceRaw) errors.push(`${path}.audience must be an object`);
2629
+ else {
2630
+ unknownKeys2(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
2631
+ if (audienceRaw.kind === "all-members") {
2632
+ if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
2633
+ } else if (audienceRaw.kind === "tiers") {
2634
+ if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
2635
+ errors.push(`${path}.audience.tierIds must be a non-empty array`);
2636
+ }
2637
+ const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
2638
+ const normalized = [];
2639
+ for (const [tierIndex, value2] of values.entries()) {
2640
+ const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
2641
+ if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
2642
+ if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
2643
+ normalized.push(tier);
2644
+ }
2645
+ audience = { kind: "tiers", tierIds: normalized };
2646
+ } else errors.push(`${path}.audience.kind must be all-members or tiers`);
2647
+ }
2648
+ const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
2649
+ const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
2650
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
2651
+ let upsell;
2652
+ if (raw.upsell !== void 0) {
2653
+ const upsellRaw = record2(raw.upsell);
2654
+ if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
2655
+ else {
2656
+ unknownKeys2(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
2657
+ const owner = upsellRaw.owner;
2658
+ if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
2659
+ if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
2660
+ if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
2661
+ if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
2662
+ const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
2663
+ if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
2664
+ else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
2665
+ }
2666
+ }
2667
+ pages.push({ id, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
2668
+ }
2669
+ const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
2670
+ for (const [index, page] of pages.entries()) {
2671
+ const parentId = page.navigation.parentId;
2672
+ if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
2673
+ if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
2674
+ }
2675
+ for (const page of pages) {
2676
+ const seen = /* @__PURE__ */ new Set([page.id]);
2677
+ let parentId = page.navigation.parentId;
2678
+ while (parentId) {
2679
+ if (seen.has(parentId)) {
2680
+ errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
2681
+ break;
2682
+ }
2683
+ seen.add(parentId);
2684
+ parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
2685
+ }
2686
+ }
2687
+ let upsellOverrides;
2688
+ if (root.upsellOverrides !== void 0) {
2689
+ if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
2690
+ if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
2691
+ const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
2692
+ const seen = /* @__PURE__ */ new Set();
2693
+ upsellOverrides = [];
2694
+ for (const [index, value] of values.entries()) {
2695
+ const path = `document.upsellOverrides[${index}]`;
2696
+ const raw = record2(value);
2697
+ if (!raw) {
2698
+ errors.push(`${path} must be an object`);
2699
+ continue;
2700
+ }
2701
+ unknownKeys2(raw, OVERRIDE_KEYS, path, errors);
2702
+ const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
2703
+ if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
2704
+ seen.add(pageId);
2705
+ const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
2706
+ if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
2707
+ else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
2708
+ const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
2709
+ if (offer) upsellOverrides.push({ pageId, offer });
2710
+ }
2711
+ }
2712
+ if (errors.length) return { ok: false, errors };
2713
+ return { ok: true, value: {
2714
+ schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
2715
+ revision: Number(root.revision),
2716
+ generatedAt: Number(root.generatedAt),
2717
+ source,
2718
+ ...target ? { target } : {},
2719
+ pages,
2720
+ ...upsellOverrides ? { upsellOverrides } : {}
2721
+ } };
2722
+ }
2723
+
2724
+ // src/member-content.ts
2725
+ var MEMBER_CONTENT_SCHEMA_VERSION = 1;
2726
+ var MEMBER_CONTENT_PATH = "/api/member-content";
2727
+ var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
2728
+ 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);
2729
+ function composeMemberContent(parent, chapter) {
2730
+ const pages = [];
2731
+ const parentPages = /* @__PURE__ */ new Map();
2732
+ for (const page of parent?.pages ?? []) {
2733
+ const effective = { ...page, source: parent.source };
2734
+ parentPages.set(page.id, effective);
2735
+ pages.push(effective);
2736
+ }
2737
+ for (const override of chapter?.upsellOverrides ?? []) {
2738
+ const page = parentPages.get(override.pageId);
2739
+ if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
2740
+ }
2741
+ for (const page of chapter?.pages ?? []) {
2742
+ if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
2743
+ throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
2744
+ }
2745
+ pages.push({ ...page, source: chapter.source });
2746
+ }
2747
+ return {
2748
+ pages: pages.sort(pageOrder),
2749
+ parentRevision: parent?.revision ?? null,
2750
+ chapterRevision: chapter?.revision ?? null
2751
+ };
2752
+ }
2753
+ var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
2754
+ function projectMemberContent(content, viewer, tiers) {
2755
+ const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
2756
+ return {
2757
+ viewer: { ...viewer },
2758
+ revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
2759
+ pages: content.pages.map((page) => {
2760
+ const canRead = allowed(page, viewer);
2761
+ const offer = !canRead ? page.upsell?.offer : void 0;
2762
+ const tier = offer ? authority.get(offer.tierId) : void 0;
2763
+ const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
2764
+ return {
2765
+ id: page.id,
2766
+ slug: page.slug,
2767
+ title: page.title,
2768
+ ...page.summary ? { summary: page.summary } : {},
2769
+ navigation: page.navigation,
2770
+ audience: page.audience,
2771
+ source: page.source,
2772
+ access: canRead ? "allowed" : "locked",
2773
+ ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
2774
+ ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
2775
+ ...upsell ? { upsell } : {}
2776
+ };
2777
+ })
2778
+ };
2779
+ }
2780
+
2781
+ // src/member-content-store.ts
2782
+ var control = (row) => row && row.document && typeof row.document === "object" ? row : null;
2783
+ var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
2784
+ async function exists(db, ns, where) {
2785
+ if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
2786
+ const rows4 = (await db.query({ [ns]: {} }))[ns] ?? [];
2787
+ return rows4.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
2788
+ }
2789
+ async function loadActiveMemberContentControl(db, targetId, environment) {
2790
+ const where = { targetId, environment, active: true };
2791
+ if (!await exists(db, "memberContentControls", where)) return null;
2792
+ const rows4 = (await db.query({ memberContentControls: { $: {
2793
+ where,
2794
+ order: { revision: "desc" },
2795
+ limit: 1
2796
+ } } })).memberContentControls;
2797
+ return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
2798
+ }
2799
+ async function loadLatestMemberContentControl(db, targetId, environment) {
2800
+ const where = { targetId, environment };
2801
+ if (!await exists(db, "memberContentControls", where)) return null;
2802
+ const rows4 = (await db.query({ memberContentControls: { $: {
2803
+ where,
2804
+ order: { revision: "desc" },
2805
+ limit: 1
2806
+ } } })).memberContentControls;
2807
+ return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
2808
+ }
2809
+ async function loadLocalMemberContent(db, chapterId, environment) {
2810
+ const id = `member-content-local:${environment}`;
2811
+ const rows4 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
2812
+ return local(Array.isArray(rows4) ? rows4[0] : void 0);
2813
+ }
2814
+ async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
2815
+ const { document, digest } = envelope;
2816
+ const target = document.target;
2817
+ const id = `member-content-control:${target.environment}:${document.revision}`;
2818
+ const ops = [];
2819
+ if (previous?.active) {
2820
+ ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
2821
+ }
2822
+ ops.push({ t: "update", ns: "memberContentControls", id, attrs: {
2823
+ id,
2824
+ sourceId: document.source.appId,
2825
+ targetId: target.chapterId,
2826
+ environment: target.environment,
2827
+ revision: document.revision,
2828
+ digest,
2829
+ document,
2830
+ active: true,
2831
+ receivedAt
2832
+ } });
2833
+ await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
2834
+ }
2835
+ async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
2836
+ const id = `member-content-local:${environment}`;
2837
+ const current = await loadLocalMemberContent(db, document.source.appId, environment);
2838
+ if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
2839
+ const row = {
2840
+ id,
2841
+ chapterId: document.source.appId,
2842
+ environment,
2843
+ revision: document.revision,
2844
+ document,
2845
+ updatedAt,
2846
+ ...updatedBy ? { updatedBy } : {}
2847
+ };
2848
+ await db.transact([{ t: "update", ns: "memberContentLocal", id, attrs: row }], {
2849
+ mutationId: `member-content-local:${environment}:${document.revision}`,
2850
+ guards: current ? [{ ns: "memberContentLocal", id, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id, exists: false }]
2851
+ });
2852
+ return row;
2853
+ }
2854
+
2855
+ // src/worker-routes-member-content.ts
2856
+ var ADMIN_PATH = "/api/admin/member-content";
2857
+ var BODY_CAP = 1e6;
2389
2858
  var reject2 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2859
+ async function groupAndTiers(db, chapterId) {
2860
+ const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
2861
+ const group = Array.isArray(groups) ? groups[0] : void 0;
2862
+ if (!group) return { group: null, tiers: [] };
2863
+ return { group, tiers: await loadTiers(db, group) };
2864
+ }
2865
+ async function memberTierId(db, chapterId, email) {
2866
+ if (!email) return null;
2867
+ let rows4 = (await db.query({ applications: { $: {
2868
+ where: { emailNormalized: normalizeAdmissionEmail(email) },
2869
+ limit: 20
2870
+ } } })).applications;
2871
+ if (!rows4?.length) rows4 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
2872
+ 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;
2873
+ return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
2874
+ }
2875
+ async function activeDocuments(db, chapterId, environment) {
2876
+ const [parent, chapter] = await Promise.all([
2877
+ loadActiveMemberContentControl(db, chapterId, environment),
2878
+ loadLocalMemberContent(db, chapterId, environment)
2879
+ ]);
2880
+ return { parent, chapter };
2881
+ }
2882
+ var handleMemberContentControl = async (req, url, env, ctx) => {
2883
+ if (req.method !== "POST" || url.pathname !== MEMBER_CONTENT_CONTROL_PATH) return null;
2884
+ const settings = ctx.chapter.memberContent?.parent;
2885
+ if (!settings) return json({ error: "not found" }, 404);
2886
+ const db = ctx.makeDb(env);
2887
+ const secret = await getVaultSecret(db, settings.secretName);
2888
+ if (!secret) return json({ error: "unauthorized" }, 401);
2889
+ const verified = await verifyFederatedRequest3(req, { secret, senders: [settings.sourceId] });
2890
+ if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2891
+ let input;
2892
+ try {
2893
+ input = JSON.parse(verified.body);
2894
+ } catch {
2895
+ return reject2("invalid_document", 400, ["request body must be JSON"]);
2896
+ }
2897
+ const digest = typeof input.digest === "string" ? input.digest : "";
2898
+ const initial = validateMemberContentDocument(input.document, {
2899
+ sourceType: "parent"
2900
+ });
2901
+ if (!initial.ok) return reject2("invalid_document", 400, initial.errors);
2902
+ const document = initial.value;
2903
+ if (document.source.appId !== settings.sourceId) return reject2("wrong_source", 403);
2904
+ if (document.target?.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
2905
+ if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
2906
+ const { tiers } = await groupAndTiers(db, ctx.chapter.id);
2907
+ const checked = validateMemberContentDocument(document, {
2908
+ sourceType: "parent",
2909
+ sourceAppId: settings.sourceId,
2910
+ tiers
2911
+ });
2912
+ if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
2913
+ const local2 = await loadLocalMemberContent(db, ctx.chapter.id, env.ODLA_ENV);
2914
+ if (local2) {
2915
+ const localCheck = validateMemberContentDocument(local2.document, {
2916
+ sourceType: "chapter",
2917
+ sourceAppId: ctx.chapter.id,
2918
+ tiers,
2919
+ parentPages: document.pages
2920
+ });
2921
+ if (!localCheck.ok) {
2922
+ return reject2("invalid_document", 409, localCheck.errors.map((error) => `existing chapter content: ${error}`));
2923
+ }
2924
+ }
2925
+ if (!digest || await memberContentDigest(document) !== digest) return reject2("invalid_digest", 400);
2926
+ const latest = await loadLatestMemberContentControl(db, ctx.chapter.id, env.ODLA_ENV);
2927
+ if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
2928
+ if (latest && document.revision === latest.revision) {
2929
+ if (latest.digest !== digest) return reject2("conflicting_revision", 409);
2930
+ return json({
2931
+ ok: true,
2932
+ target: document.target,
2933
+ revision: latest.revision,
2934
+ digest: latest.digest,
2935
+ status: "replayed",
2936
+ acknowledgedAt: latest.receivedAt
2937
+ });
2938
+ }
2939
+ const receivedAt = Date.now();
2940
+ try {
2941
+ await materializeMemberContentControl(db, { digest, document }, latest, receivedAt);
2942
+ } catch {
2943
+ return reject2("materialization_failed", 503);
2944
+ }
2945
+ return json({
2946
+ ok: true,
2947
+ target: document.target,
2948
+ revision: document.revision,
2949
+ digest,
2950
+ status: "accepted",
2951
+ acknowledgedAt: receivedAt
2952
+ });
2953
+ };
2954
+ var handleMemberContent = async (req, url, env, ctx) => {
2955
+ if (req.method !== "GET" || url.pathname !== MEMBER_CONTENT_PATH) return null;
2956
+ if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
2957
+ const user = await ctx.verifyUser(req, env);
2958
+ if (!user) return json({ error: "unauthorized" }, 401);
2959
+ const db = ctx.makeDb(env);
2960
+ const role = await ctx.roleFor(db, user);
2961
+ const admin = await ctx.isAdmin(db, user);
2962
+ const roleIndex = ctx.chapter.auth.ladder.indexOf(role);
2963
+ const memberIndex = Math.max(0, ctx.chapter.auth.ladder.length - 2);
2964
+ if (!admin && roleIndex < memberIndex) return json({ error: "member access required" }, 403);
2965
+ const [{ parent, chapter }, { group, tiers }, tierId] = await Promise.all([
2966
+ activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
2967
+ groupAndTiers(db, ctx.chapter.id),
2968
+ memberTierId(db, ctx.chapter.id, user.email)
2969
+ ]);
2970
+ const effective = composeMemberContent(parent?.document, chapter?.document);
2971
+ let offerable = tiers;
2972
+ if (group) {
2973
+ const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
2974
+ offerable = await loadOfferableTiers(db, group, Boolean(stripeSecret));
2975
+ }
2976
+ return json(projectMemberContent(effective, { tierId, admin }, offerable));
2977
+ };
2978
+ var handleAdminMemberContent = async (req, url, env, ctx) => {
2979
+ if (url.pathname !== ADMIN_PATH) return null;
2980
+ if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
2981
+ const user = await ctx.verifyUser(req, env);
2982
+ if (!user) return json({ error: "unauthorized" }, 401);
2983
+ const db = ctx.makeDb(env);
2984
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
2985
+ const [{ parent, chapter }, { tiers }] = await Promise.all([
2986
+ activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
2987
+ groupAndTiers(db, ctx.chapter.id)
2988
+ ]);
2989
+ if (req.method === "GET") {
2990
+ const effective = composeMemberContent(parent?.document, chapter?.document);
2991
+ return json({
2992
+ parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
2993
+ chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
2994
+ effective,
2995
+ tiers: tiers.map(({ id, name, priceCents, active }) => ({ id, name, priceCents, active })),
2996
+ localAuthoring: ctx.chapter.memberContent.localAuthoring
2997
+ });
2998
+ }
2999
+ if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
3000
+ if (!ctx.chapter.memberContent.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
3001
+ const raw = await req.text();
3002
+ if (raw.length > BODY_CAP) return json({ error: "request body too large" }, 413);
3003
+ let input;
3004
+ try {
3005
+ input = JSON.parse(raw);
3006
+ } catch {
3007
+ return json({ error: "invalid JSON body" }, 400);
3008
+ }
3009
+ const expectedRevision = Number(input.expectedRevision);
3010
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 0) {
3011
+ return json({ error: "expectedRevision must be a non-negative integer" }, 400);
3012
+ }
3013
+ const now = Date.now();
3014
+ const documentInput = withLocalAuthority(input.document, expectedRevision + 1, now, ctx.chapter.id);
3015
+ const checked = validateMemberContentDocument(documentInput, {
3016
+ sourceType: "chapter",
3017
+ sourceAppId: ctx.chapter.id,
3018
+ tiers,
3019
+ parentPages: parent?.document.pages ?? []
3020
+ });
3021
+ if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
3022
+ try {
3023
+ const saved = await saveLocalMemberContent(
3024
+ db,
3025
+ checked.value,
3026
+ env.ODLA_ENV,
3027
+ expectedRevision,
3028
+ now,
3029
+ user.email ?? user.userId
3030
+ );
3031
+ return json({ ok: true, revision: saved.revision, document: saved.document });
3032
+ } catch (error) {
3033
+ if (error instanceof Error && error.message === "member_content_revision_conflict") {
3034
+ return json({ error: "member content changed; reload before saving" }, 409);
3035
+ }
3036
+ return json({ error: "member content could not be saved" }, 503);
3037
+ }
3038
+ };
3039
+ function withLocalAuthority(value, revision, generatedAt, appId) {
3040
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
3041
+ return {
3042
+ ...value,
3043
+ schemaVersion: 1,
3044
+ revision,
3045
+ generatedAt,
3046
+ source: { type: "chapter", appId }
3047
+ };
3048
+ }
3049
+
3050
+ // src/worker-routes-admission-grants.ts
3051
+ import { verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
3052
+ var reject3 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2390
3053
  var handleAdmissionGrants = async (req, url, env, ctx) => {
2391
3054
  if (req.method !== "POST" || url.pathname !== ADMISSION_GRANTS_PATH) return null;
2392
3055
  const authority = ctx.chapter.signupControl;
@@ -2394,26 +3057,26 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
2394
3057
  const db = ctx.makeDb(env);
2395
3058
  const secret = await getVaultSecret(db, authority.secretName);
2396
3059
  if (!secret) return json({ error: "unauthorized" }, 401);
2397
- const verified = await verifyFederatedRequest3(req, { secret, senders: [authority.sourceId] });
3060
+ const verified = await verifyFederatedRequest4(req, { secret, senders: [authority.sourceId] });
2398
3061
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2399
3062
  let input;
2400
3063
  try {
2401
3064
  input = JSON.parse(verified.body);
2402
3065
  } catch {
2403
- return reject2("invalid_document", 400, ["request body must be JSON"]);
3066
+ return reject3("invalid_document", 400, ["request body must be JSON"]);
2404
3067
  }
2405
3068
  const digest = typeof input.digest === "string" ? input.digest : "";
2406
3069
  const checked = validateAdmissionGrantDocument(input.document);
2407
- if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
3070
+ if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
2408
3071
  const document = checked.value;
2409
- if (document.source.appId !== authority.sourceId) return reject2("wrong_source", 403);
2410
- if (document.target.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
2411
- if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
2412
- if (!digest || await signupControlDigest(document) !== digest) return reject2("invalid_digest", 400);
3072
+ if (document.source.appId !== authority.sourceId) return reject3("wrong_source", 403);
3073
+ if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
3074
+ if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
3075
+ if (!digest || await signupControlDigest(document) !== digest) return reject3("invalid_digest", 400);
2413
3076
  const latest = await loadLatestAdmissionRevision(db, authority.sourceId, ctx.chapter.id, env.ODLA_ENV);
2414
- if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
3077
+ if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
2415
3078
  if (latest && document.revision === latest.revision) {
2416
- if (latest.digest !== digest) return reject2("conflicting_revision", 409);
3079
+ if (latest.digest !== digest) return reject3("conflicting_revision", 409);
2417
3080
  return json({
2418
3081
  ok: true,
2419
3082
  target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
@@ -2427,7 +3090,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
2427
3090
  try {
2428
3091
  await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
2429
3092
  } catch {
2430
- return reject2("materialization_failed", 503);
3093
+ return reject3("materialization_failed", 503);
2431
3094
  }
2432
3095
  return json({
2433
3096
  ok: true,
@@ -2469,7 +3132,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
2469
3132
  return json({ error: "invalid JSON body" }, 400);
2470
3133
  }
2471
3134
  const email = typeof body.email === "string" ? body.email.trim() : "";
2472
- const emailNormalized = normalizeAdmissionEmail(email);
3135
+ const emailNormalized = normalizeChapterEmail(email);
2473
3136
  if (!isValidEmail(emailNormalized)) return json({ error: "email must be valid" }, 400);
2474
3137
  const decision = body.decision ?? "waive_interview";
2475
3138
  if (decision !== "waive_interview" && decision !== "require_interview") {
@@ -2971,7 +3634,7 @@ async function rotateGiftClaim(db, claim, now) {
2971
3634
  async function redeemGiftClaim(db, claim, beneficiary, now) {
2972
3635
  if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) return null;
2973
3636
  const chapterId = String(claim.chapterId);
2974
- const emailNormalized = normalizeAdmissionEmail(beneficiary.email);
3637
+ const emailNormalized = normalizeChapterEmail(beneficiary.email);
2975
3638
  const target = claim.target;
2976
3639
  const ops = [];
2977
3640
  let result;
@@ -3255,8 +3918,8 @@ async function startSubscription(req, env, ctx) {
3255
3918
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
3256
3919
  if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
3257
3920
  if (typeof app.giftOfferId === "string") {
3258
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3259
- const offer = findGiftOffer(control?.document.giftOffers, app.giftOfferId, Date.now());
3921
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3922
+ const offer = findGiftOffer(control2?.document.giftOffers, app.giftOfferId, Date.now());
3260
3923
  if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
3261
3924
  return json({ error: "gift offer is unavailable" }, 409);
3262
3925
  }
@@ -3268,7 +3931,7 @@ async function startSubscription(req, env, ctx) {
3268
3931
  offer,
3269
3932
  purchaserType: "application",
3270
3933
  purchaserEmail: String(app.email ?? ""),
3271
- purchaserEmailNormalized: normalizeAdmissionEmail(String(app.email ?? "")),
3934
+ purchaserEmailNormalized: normalizeChapterEmail(String(app.email ?? "")),
3272
3935
  purchaserApplicationId: applicationId,
3273
3936
  stripeSecret: secretKey,
3274
3937
  publishableKey: group.stripePublishableKey,
@@ -3415,14 +4078,14 @@ var handleGifts = async (req, url, env, ctx) => {
3415
4078
  if (req.method === "GET" && url.pathname === "/api/gifts") {
3416
4079
  const user = await userFor(req, env, ctx);
3417
4080
  if (!user) return json({ error: "unauthorized" }, 401);
3418
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4081
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3419
4082
  const found = await db.query({ giftClaims: { $: { where: {
3420
- purchaserEmailNormalized: normalizeAdmissionEmail(user.email),
4083
+ purchaserEmailNormalized: normalizeChapterEmail(user.email),
3421
4084
  chapterId: ctx.chapter.id
3422
4085
  }, limit: 200 } } });
3423
4086
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3424
4087
  return json({
3425
- offers: publicGiftOffers(control?.document.giftOffers, Date.now()),
4088
+ offers: publicGiftOffers(control2?.document.giftOffers, Date.now()),
3426
4089
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
3427
4090
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
3428
4091
  });
@@ -3438,13 +4101,13 @@ var handleGifts = async (req, url, env, ctx) => {
3438
4101
  }
3439
4102
  const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
3440
4103
  if (!/^[A-Za-z0-9_-]{16,128}$/.test(purchaseId)) return json({ error: "purchaseId is invalid" }, 400);
3441
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3442
- const offer = findGiftOffer(control?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
4104
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4105
+ const offer = findGiftOffer(control2?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
3443
4106
  if (!offer) return json({ error: "gift offer is unavailable" }, 409);
3444
4107
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3445
4108
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
3446
4109
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
3447
- const emailNormalized = normalizeAdmissionEmail(user.email);
4110
+ const emailNormalized = normalizeChapterEmail(user.email);
3448
4111
  const applications = rows2((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
3449
4112
  const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
3450
4113
  try {
@@ -3486,7 +4149,7 @@ var handleGifts = async (req, url, env, ctx) => {
3486
4149
  return invalid();
3487
4150
  }
3488
4151
  }
3489
- const owns = user ? claim.purchaserEmailNormalized === normalizeAdmissionEmail(user.email) : claim.purchaserApplicationId === applicationId;
4152
+ const owns = user ? claim.purchaserEmailNormalized === normalizeChapterEmail(user.email) : claim.purchaserApplicationId === applicationId;
3490
4153
  if (!owns) return invalid();
3491
4154
  const rotated = await rotateGiftClaim(db, claim, Date.now());
3492
4155
  return rotated ? json(rotated) : invalid();
@@ -4360,7 +5023,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
4360
5023
 
4361
5024
  // src/worker-routes-network.ts
4362
5025
  import { getRecord, typeSummary } from "@odla-ai/crm";
4363
- import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
5026
+ import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
4364
5027
 
4365
5028
  // src/worker-network-fetch.ts
4366
5029
  function targetFetcher(env, target) {
@@ -4379,7 +5042,7 @@ function fetchNetworkTarget(env, target, destination, init) {
4379
5042
  // src/worker-network-push.ts
4380
5043
  import { recordDeliveryAttempt } from "@odla-ai/crm";
4381
5044
  import { signFederatedRequest } from "@odla-ai/db";
4382
- async function pushNetworkRecord(db, env, ctx, target, record2) {
5045
+ async function pushNetworkRecord(db, env, ctx, target, record3) {
4383
5046
  const secret = await getVaultSecret(db, target.secretName);
4384
5047
  const crmDeps3 = {
4385
5048
  crm: ctx.chapter.crm,
@@ -4390,7 +5053,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4390
5053
  if (!secret) {
4391
5054
  const error = `vault secret "${target.secretName}" is missing`;
4392
5055
  await recordDeliveryAttempt(crmDeps3, {
4393
- recordId: record2.id,
5056
+ recordId: record3.id,
4394
5057
  targetId: target.id,
4395
5058
  status: "failed",
4396
5059
  payloadVersion: 2,
@@ -4400,7 +5063,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4400
5063
  }
4401
5064
  let payload;
4402
5065
  try {
4403
- payload = sharedRecordFromCrm(ctx.chapter.crm, record2, target, ctx.chapter.id);
5066
+ payload = sharedRecordFromCrm(ctx.chapter.crm, record3, target, ctx.chapter.id);
4404
5067
  } catch (err) {
4405
5068
  return {
4406
5069
  id: target.id,
@@ -4429,7 +5092,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4429
5092
  if (!res.ok) {
4430
5093
  const error = responseBody.error ?? "follower rejected the record";
4431
5094
  await recordDeliveryAttempt(crmDeps3, {
4432
- recordId: record2.id,
5095
+ recordId: record3.id,
4433
5096
  targetId: target.id,
4434
5097
  status: "failed",
4435
5098
  payloadVersion: 2,
@@ -4444,7 +5107,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4444
5107
  };
4445
5108
  }
4446
5109
  await recordDeliveryAttempt(crmDeps3, {
4447
- recordId: record2.id,
5110
+ recordId: record3.id,
4448
5111
  targetId: target.id,
4449
5112
  status: "delivered",
4450
5113
  payloadVersion: 2,
@@ -4460,7 +5123,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4460
5123
  } catch (err) {
4461
5124
  const error = err instanceof Error ? err.message : "delivery failed";
4462
5125
  await recordDeliveryAttempt(crmDeps3, {
4463
- recordId: record2.id,
5126
+ recordId: record3.id,
4464
5127
  targetId: target.id,
4465
5128
  status: "failed",
4466
5129
  payloadVersion: 2,
@@ -4497,7 +5160,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
4497
5160
  const db = ctx.makeDb(env);
4498
5161
  const secret = await getVaultSecret(db, "network_share_secret");
4499
5162
  if (!secret) return json({ error: "unauthorized" }, 401);
4500
- const verified = await verifyFederatedRequest4(req, { secret });
5163
+ const verified = await verifyFederatedRequest5(req, { secret });
4501
5164
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
4502
5165
  const types = await Promise.all(
4503
5166
  Object.keys(ctx.chapter.crm.config.types).map((type) => typeSummary({ crm: ctx.chapter.crm, db }, type))
@@ -4598,17 +5261,17 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
4598
5261
  if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
4599
5262
  const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
4600
5263
  if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
4601
- const record2 = await getRecord({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
4602
- if (!record2) return json({ error: "record not found" }, 404);
5264
+ const record3 = await getRecord({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
5265
+ if (!record3) return json({ error: "record not found" }, 404);
4603
5266
  const results = await Promise.all(
4604
- targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record2))
5267
+ targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record3))
4605
5268
  );
4606
5269
  return json({ ok: results.every((result) => result.ok), results });
4607
5270
  };
4608
5271
 
4609
5272
  // src/worker-routes-network-records.ts
4610
5273
  import { listRecords } from "@odla-ai/crm";
4611
- import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
5274
+ import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest6 } from "@odla-ai/db";
4612
5275
  var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
4613
5276
  var MAX_PAGE = 50;
4614
5277
  var MAX_OFFSET = 1e4;
@@ -4626,18 +5289,18 @@ function integer(value, fallback, min, max) {
4626
5289
  const parsed = Number(value);
4627
5290
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
4628
5291
  }
4629
- function projectRecord(record2, allowed) {
5292
+ function projectRecord(record3, allowed2) {
4630
5293
  const fields = Object.fromEntries(
4631
- allowed.flatMap((name) => Object.prototype.hasOwnProperty.call(record2.fields, name) ? [[name, record2.fields[name]]] : [])
5294
+ allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record3.fields, name) ? [[name, record3.fields[name]]] : [])
4632
5295
  );
4633
5296
  return {
4634
- id: record2.id,
4635
- type: record2.type,
4636
- name: record2.name,
4637
- ...record2.stage ? { stage: record2.stage } : {},
5297
+ id: record3.id,
5298
+ type: record3.type,
5299
+ name: record3.name,
5300
+ ...record3.stage ? { stage: record3.stage } : {},
4638
5301
  fields,
4639
- createdAt: record2.createdAt,
4640
- updatedAt: record2.updatedAt
5302
+ createdAt: record3.createdAt,
5303
+ updatedAt: record3.updatedAt
4641
5304
  };
4642
5305
  }
4643
5306
  var handleNetworkRecords = async (req, url, env, ctx) => {
@@ -4646,15 +5309,15 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
4646
5309
  const secret = await getVaultSecret(db, "network_share_secret");
4647
5310
  const readers = ctx.chapter.network.readers;
4648
5311
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
4649
- const verified = await verifyFederatedRequest5(req, {
5312
+ const verified = await verifyFederatedRequest6(req, {
4650
5313
  secret,
4651
5314
  senders: readers.map((reader2) => reader2.id)
4652
5315
  });
4653
5316
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
4654
5317
  const reader = readers.find((candidate) => candidate.id === verified.sender);
4655
5318
  const type = url.searchParams.get("type") ?? "";
4656
- const allowed = reader?.fields[type];
4657
- if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
5319
+ const allowed2 = reader?.fields[type];
5320
+ if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
4658
5321
  const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
4659
5322
  const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
4660
5323
  const search = (url.searchParams.get("search") ?? "").trim();
@@ -4675,7 +5338,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
4675
5338
  version: 1,
4676
5339
  site: { id: ctx.chapter.id, name: ctx.chapter.name },
4677
5340
  type,
4678
- records: page.records.map((record2) => projectRecord(record2, allowed)),
5341
+ records: page.records.map((record3) => projectRecord(record3, allowed2)),
4679
5342
  total: page.total,
4680
5343
  limit: page.limit,
4681
5344
  offset: page.offset
@@ -4830,7 +5493,7 @@ import {
4830
5493
  CRM_NS,
4831
5494
  getRecord as getRecord2
4832
5495
  } from "@odla-ai/crm";
4833
- import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest6 } from "@odla-ai/db";
5496
+ import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest7 } from "@odla-ai/db";
4834
5497
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
4835
5498
  var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
4836
5499
  var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
@@ -4889,7 +5552,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
4889
5552
  const secret = await getVaultSecret(db, "network_share_secret");
4890
5553
  const readers = ctx.chapter.network.readers;
4891
5554
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
4892
- const verified = await verifyFederatedRequest6(req, {
5555
+ const verified = await verifyFederatedRequest7(req, {
4893
5556
  secret,
4894
5557
  senders: readers.map((reader2) => reader2.id)
4895
5558
  });
@@ -4900,8 +5563,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
4900
5563
  if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
4901
5564
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
4902
5565
  }
4903
- const record2 = await getRecord2({ crm: ctx.chapter.crm, db }, recordId);
4904
- if (!record2 || record2.type !== recordType) return json({ error: "record not found" }, 404);
5566
+ const record3 = await getRecord2({ crm: ctx.chapter.crm, db }, recordId);
5567
+ if (!record3 || record3.type !== recordType) return json({ error: "record not found" }, 404);
4905
5568
  if (req.method === "GET") {
4906
5569
  return json({
4907
5570
  version: 1,
@@ -5014,31 +5677,17 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5014
5677
 
5015
5678
  // src/worker-routes-formation.ts
5016
5679
  import { createRecord as createRecord3 } from "@odla-ai/crm";
5017
-
5018
- // src/formation.ts
5019
- function formationFields(crm, formation) {
5020
- const def = crm.type(formation.type);
5021
- return [...formation.required, ...formation.optional].map((id) => {
5022
- const field = def.fields[id];
5023
- return {
5024
- id,
5025
- label: field.label ?? id,
5026
- type: field.type,
5027
- required: formation.required.includes(id),
5028
- ...field.options ? { options: field.options } : {}
5029
- };
5030
- });
5031
- }
5032
-
5033
- // src/worker-routes-formation.ts
5034
5680
  var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
5035
5681
  var handleFormation = async (req, url, env, ctx) => {
5036
5682
  const formation = ctx.chapter.formation;
5037
5683
  if (!formation.enabled) return null;
5038
5684
  if (req.method === "GET" && url.pathname === "/api/formation/config") {
5685
+ const fieldContract = chapterFlowFieldContract(ctx.chapter, "formation");
5039
5686
  return json({
5040
5687
  type: formation.type,
5041
- fields: formationFields(ctx.chapter.crm, formation)
5688
+ fieldContract,
5689
+ // Backward compatibility for hosts that consumed the original array.
5690
+ fields: fieldContract.fields
5042
5691
  });
5043
5692
  }
5044
5693
  if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
@@ -5055,15 +5704,15 @@ var handleFormation = async (req, url, env, ctx) => {
5055
5704
  if (submissionId && !SUBMISSION.test(submissionId)) {
5056
5705
  return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
5057
5706
  }
5058
- const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
5059
- const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
5707
+ const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
5708
+ const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed2.has(field));
5060
5709
  if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
5061
5710
  for (const field of formation.required) {
5062
5711
  const value = body[field];
5063
5712
  if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
5064
5713
  }
5065
5714
  const input = {};
5066
- for (const field of allowed) {
5715
+ for (const field of allowed2) {
5067
5716
  const value = body[field];
5068
5717
  if (value === void 0) continue;
5069
5718
  const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
@@ -5131,12 +5780,37 @@ async function deliverSignupControl(options) {
5131
5780
  return result;
5132
5781
  }
5133
5782
 
5134
- // src/admission-grants-delivery.ts
5783
+ // src/member-content-delivery.ts
5135
5784
  import { signFederatedRequest as signFederatedRequest6 } from "@odla-ai/db";
5785
+ async function deliverMemberContent(options) {
5786
+ const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
5787
+ const body = JSON.stringify(options.envelope);
5788
+ const signed = await signFederatedRequest6({
5789
+ secret: options.secret,
5790
+ sender: options.sender,
5791
+ method: "POST",
5792
+ url: destination,
5793
+ body
5794
+ });
5795
+ const response = await (options.fetch ?? globalThis.fetch)(destination, {
5796
+ method: "POST",
5797
+ headers: { ...signed, "content-type": "application/json" },
5798
+ body
5799
+ });
5800
+ const result = await response.json();
5801
+ if (!response.ok || !("ok" in result) || result.ok !== true) {
5802
+ const failure = result;
5803
+ throw new Error(`member content rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
5804
+ }
5805
+ return result;
5806
+ }
5807
+
5808
+ // src/admission-grants-delivery.ts
5809
+ import { signFederatedRequest as signFederatedRequest7 } from "@odla-ai/db";
5136
5810
  async function deliverAdmissionGrants(options) {
5137
5811
  const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
5138
5812
  const body = JSON.stringify(options.envelope);
5139
- const signed = await signFederatedRequest6({
5813
+ const signed = await signFederatedRequest7({
5140
5814
  secret: options.secret,
5141
5815
  sender: options.sender,
5142
5816
  method: "POST",
@@ -5295,12 +5969,14 @@ var BUILTIN_ROUTES = [
5295
5969
  handleCrm,
5296
5970
  handleNetworkShared,
5297
5971
  handleSignupControl,
5972
+ handleMemberContentControl,
5298
5973
  handleAdmissionGrants,
5299
5974
  handleNetworkSnapshot,
5300
5975
  handleNetworkRecords,
5301
5976
  handleNetworkSharedNotes,
5302
5977
  handleFormation,
5303
5978
  handleMember,
5979
+ handleMemberContent,
5304
5980
  handleGifts,
5305
5981
  handleSchedule,
5306
5982
  handlePayments,
@@ -5316,6 +5992,7 @@ var BUILTIN_ROUTES = [
5316
5992
  // Aggregation
5317
5993
  handleAdminDashboard,
5318
5994
  handleAdminBilling,
5995
+ handleAdminMemberContent,
5319
5996
  // Lifecycle actions
5320
5997
  handleAdminMeetingReschedule,
5321
5998
  handleAdminMeetingCancel,
@@ -5357,6 +6034,7 @@ export {
5357
6034
  chapterWorker,
5358
6035
  createWorkerContext,
5359
6036
  deliverAdmissionGrants,
6037
+ deliverMemberContent,
5360
6038
  deliverSignupControl,
5361
6039
  renderActiveSignupControlHtml,
5362
6040
  withActiveSignupControlTheme