@odla-ai/chapter 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,7 @@ __export(worker_exports, {
23
23
  chapterWorker: () => chapterWorker,
24
24
  createWorkerContext: () => createWorkerContext,
25
25
  deliverAdmissionGrants: () => deliverAdmissionGrants,
26
+ deliverMemberContent: () => deliverMemberContent,
26
27
  deliverSignupControl: () => deliverSignupControl,
27
28
  renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
28
29
  withActiveSignupControlTheme: () => withActiveSignupControlTheme
@@ -210,9 +211,9 @@ function canonicalValue(value) {
210
211
  }
211
212
  if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
212
213
  if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
213
- const record2 = value;
214
- const entries = Object.keys(record2).sort().flatMap((key) => {
215
- const item = record2[key];
214
+ const record3 = value;
215
+ const entries = Object.keys(record3).sort().flatMap((key) => {
216
+ const item = record3[key];
216
217
  return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
217
218
  });
218
219
  return `{${entries.join(",")}}`;
@@ -462,14 +463,14 @@ function hasDisclaimerAck(fields) {
462
463
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
463
464
  function applicantProfile(chapter, fields) {
464
465
  const app = chapter.application;
465
- const allowed = (f) => app.profileFields.includes(f);
466
+ const allowed2 = (f) => app.profileFields.includes(f);
466
467
  const profile = {};
467
468
  for (const f of [...app.required, ...app.optional]) {
468
- if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
469
+ if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
469
470
  const v = fields[f];
470
471
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
471
472
  }
472
- if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
473
+ if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
473
474
  return Object.keys(profile).length > 0 ? profile : void 0;
474
475
  }
475
476
  async function submitApplication(db, chapter, fields, opts) {
@@ -762,8 +763,8 @@ function validateGiftOfferCatalog(input, tierIds) {
762
763
  errors.push(`${path} must be an object`);
763
764
  continue;
764
765
  }
765
- const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
766
- for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
766
+ const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
767
+ for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
767
768
  const id = text(item.id, `${path}.id`, errors, 80);
768
769
  if (id && !ID2.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
769
770
  if (ids.has(id)) errors.push(`${path}.id duplicates "${id}"`);
@@ -860,8 +861,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
860
861
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
861
862
  const page = Array.isArray(data) ? data : [];
862
863
  for (const u of page) {
863
- const record2 = toRecord(u);
864
- if (record2) out.push(record2);
864
+ const record3 = toRecord(u);
865
+ if (record3) out.push(record3);
865
866
  }
866
867
  if (page.length < PAGE) break;
867
868
  }
@@ -918,34 +919,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
918
919
  if (readable && raw.length <= 64) return raw;
919
920
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
920
921
  }
921
- function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
922
- if ("input" in record2 && record2.version === 2) {
922
+ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
923
+ if ("input" in record3 && record3.version === 2) {
923
924
  return {
924
925
  version: 2,
925
- sourceId: record2.source.siteId,
926
- sourceRecordId: record2.source.recordId,
927
- type: record2.type,
928
- input: record2.input
926
+ sourceId: record3.source.siteId,
927
+ sourceRecordId: record3.source.recordId,
928
+ type: record3.type,
929
+ input: record3.input
929
930
  };
930
931
  }
931
- if ("input" in record2) {
932
+ if ("input" in record3) {
932
933
  return {
933
934
  version: 1,
934
935
  sourceId: fallbackSourceId,
935
- sourceRecordId: record2.hubRecordId,
936
- type: record2.type,
937
- input: record2.input
936
+ sourceRecordId: record3.hubRecordId,
937
+ type: record3.type,
938
+ input: record3.input
938
939
  };
939
940
  }
940
- if ("type" in record2 && record2.type === "company") {
941
- const input = { name: record2.name };
941
+ if ("type" in record3 && record3.type === "company") {
942
+ const input = { name: record3.name };
942
943
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
943
- if (record2[key]) input[key] = record2[key];
944
+ if (record3[key]) input[key] = record3[key];
944
945
  }
945
946
  return {
946
947
  version: 1,
947
948
  sourceId: fallbackSourceId,
948
- sourceRecordId: record2.hubRecordId,
949
+ sourceRecordId: record3.hubRecordId,
949
950
  type: "company",
950
951
  input
951
952
  };
@@ -953,28 +954,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
953
954
  return {
954
955
  version: 1,
955
956
  sourceId: fallbackSourceId,
956
- sourceRecordId: record2.hubRecordId,
957
+ sourceRecordId: record3.hubRecordId,
957
958
  type: "person",
958
- input: sharedPersonInput(record2)
959
+ input: sharedPersonInput(record3)
959
960
  };
960
961
  }
961
- function sharedRecordFromCrm(crm, record2, target, sourceId) {
962
- if (target.fields && !target.fields[record2.type]) {
963
- throw new Error(`${target.name} does not accept "${record2.type}" records`);
962
+ function sharedRecordFromCrm(crm, record3, target, sourceId) {
963
+ if (target.fields && !target.fields[record3.type]) {
964
+ throw new Error(`${target.name} does not accept "${record3.type}" records`);
964
965
  }
965
- const def = crm.type(record2.type);
966
- const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
966
+ const def = crm.type(record3.type);
967
+ const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
967
968
  if (!fields) {
968
- throw new Error(`${target.name} requires an explicit field allowlist for "${record2.type}" records`);
969
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
969
970
  }
970
971
  const nameField = def.nameField ?? "name";
971
972
  const input = {};
972
973
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
973
- const value = record2.fields?.[field];
974
+ const value = record3.fields?.[field];
974
975
  if (value !== void 0) input[field] = value;
975
976
  }
976
- if (input[nameField] === void 0) input[nameField] = record2.name;
977
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
977
+ if (input[nameField] === void 0) input[nameField] = record3.name;
978
+ return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
978
979
  }
979
980
  async function upsertPerson(deps, opts) {
980
981
  const email = opts.email.toLowerCase();
@@ -988,11 +989,11 @@ async function upsertPerson(deps, opts) {
988
989
  const created = await (0, import_crm2.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
989
990
  return { recordId: created.id };
990
991
  }
991
- async function findSharedRecord(deps, record2, tag) {
992
+ async function findSharedRecord(deps, record3, tag) {
992
993
  const structured = await (0, import_crm2.getRecordByOrigin)(
993
994
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
994
- record2.sourceId,
995
- record2.sourceRecordId
995
+ record3.sourceId,
996
+ record3.sourceRecordId
996
997
  );
997
998
  if (structured) return structured;
998
999
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -1001,53 +1002,53 @@ async function findSharedRecord(deps, record2, tag) {
1001
1002
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1002
1003
  if (found.crm_record?.[0]) return found.crm_record[0];
1003
1004
  }
1004
- const def = deps.crm.type(record2.type);
1005
+ const def = deps.crm.type(record3.type);
1005
1006
  const emailField = def.emailField;
1006
- if (emailField && typeof record2.input[emailField] === "string") {
1007
- const primaryEmail = record2.input[emailField].toLowerCase();
1007
+ if (emailField && typeof record3.input[emailField] === "string") {
1008
+ const primaryEmail = record3.input[emailField].toLowerCase();
1008
1009
  const found = await deps.db.query({
1009
- crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
1010
+ crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
1010
1011
  });
1011
1012
  if (found.crm_record?.[0]) return found.crm_record[0];
1012
1013
  }
1013
- const domain = record2.input.domain;
1014
+ const domain = record3.input.domain;
1014
1015
  const domainSlot = def.fields.domain?.slot;
1015
1016
  if (typeof domain === "string" && domainSlot) {
1016
1017
  const found = await deps.db.query({
1017
- crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
1018
+ crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
1018
1019
  });
1019
1020
  if (found.crm_record?.[0]) return found.crm_record[0];
1020
1021
  }
1021
1022
  const nameField = def.nameField ?? "name";
1022
- const name = record2.input[nameField];
1023
- if (record2.type === "company" && typeof name === "string" && name.trim()) {
1023
+ const name = record3.input[nameField];
1024
+ if (record3.type === "company" && typeof name === "string" && name.trim()) {
1024
1025
  const found = await deps.db.query({
1025
- crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
1026
+ crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
1026
1027
  });
1027
1028
  if (found.crm_record?.[0]) return found.crm_record[0];
1028
1029
  }
1029
1030
  return void 0;
1030
1031
  }
1031
1032
  async function projectSharedRecord(deps, shared, options = {}) {
1032
- const record2 = normalizeSharedRecord(shared, options.sourceId);
1033
- if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
1033
+ const record3 = normalizeSharedRecord(shared, options.sourceId);
1034
+ if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
1034
1035
  throw new Error("type, source site id, and source record id must be non-empty");
1035
1036
  }
1036
- if (options.sourceId && options.sourceId !== record2.sourceId) {
1037
+ if (options.sourceId && options.sourceId !== record3.sourceId) {
1037
1038
  throw new Error("signed sender does not match payload source");
1038
1039
  }
1039
- const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
1040
+ const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
1040
1041
  const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1041
- const existing = await findSharedRecord(deps, record2, tag);
1042
+ const existing = await findSharedRecord(deps, record3, tag);
1042
1043
  let recordId;
1043
1044
  if (existing && typeof existing.id === "string") {
1044
- await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record2.input });
1045
+ await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record3.input });
1045
1046
  recordId = existing.id;
1046
1047
  } else {
1047
- recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
1048
+ recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
1048
1049
  await (0, import_crm2.createRecord)({ ...crmDeps3, newId: () => recordId }, {
1049
- type: record2.type,
1050
- input: record2.input,
1050
+ type: record3.type,
1051
+ input: record3.input,
1051
1052
  mutationId: `share-create:${tag}`
1052
1053
  });
1053
1054
  }
@@ -1057,10 +1058,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
1057
1058
  );
1058
1059
  await (0, import_crm2.upsertRecordOrigin)(crmDeps3, {
1059
1060
  recordId,
1060
- sourceId: record2.sourceId,
1061
- sourceRecordId: record2.sourceRecordId,
1061
+ sourceId: record3.sourceId,
1062
+ sourceRecordId: record3.sourceRecordId,
1062
1063
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
1063
- payloadVersion: record2.version
1064
+ payloadVersion: record3.version
1064
1065
  });
1065
1066
  return { recordId };
1066
1067
  }
@@ -1509,7 +1510,8 @@ function applicationSummary(app) {
1509
1510
  meetingLink: app.meetingLink ?? null,
1510
1511
  paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
1511
1512
  renewalAt: app.renewalAt ?? null,
1512
- canceled: app.canceled === true
1513
+ canceled: app.canceled === true,
1514
+ tierId: app.tierId ?? null
1513
1515
  };
1514
1516
  }
1515
1517
  function memberApplication(app, meeting, defaultTimezone) {
@@ -1983,8 +1985,8 @@ var handleNetworkShared = async (req, url, env, ctx) => {
1983
1985
  payload,
1984
1986
  sourceId ? { sourceId } : {}
1985
1987
  );
1986
- const record2 = normalizeSharedRecord(payload, sourceId);
1987
- return json({ recordId, type: record2.type });
1988
+ const record3 = normalizeSharedRecord(payload, sourceId);
1989
+ return json({ recordId, type: record3.type });
1988
1990
  } catch (error) {
1989
1991
  return json({
1990
1992
  error: error instanceof Error ? error.message : "invalid shared record"
@@ -2024,9 +2026,9 @@ var MAX_BENEFITS = 24;
2024
2026
  var MAX_MARKDOWN = 65536;
2025
2027
  var MAX_LABEL = 160;
2026
2028
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2027
- function rejectUnknownKeys(value, allowed, path, errors) {
2029
+ function rejectUnknownKeys(value, allowed2, path, errors) {
2028
2030
  for (const key of Object.keys(value)) {
2029
- if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2031
+ if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2030
2032
  }
2031
2033
  }
2032
2034
  function boundedString(value, path, errors, max, required = false) {
@@ -2182,8 +2184,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2182
2184
  ]);
2183
2185
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2184
2186
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2185
- function unknownKeys(value, allowed, path, errors) {
2186
- for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2187
+ function unknownKeys(value, allowed2, path, errors) {
2188
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2187
2189
  }
2188
2190
  function text2(value, path, errors, max = 65536) {
2189
2191
  if (typeof value !== "string" || !value.trim()) {
@@ -2268,10 +2270,10 @@ function policyOf(value, errors) {
2268
2270
  errors.push("document.policy must be an object");
2269
2271
  return void 0;
2270
2272
  }
2271
- const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2272
- unknownKeys(input, allowed, "document.policy", errors);
2273
+ const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2274
+ unknownKeys(input, allowed2, "document.policy", errors);
2273
2275
  const result = {};
2274
- for (const key of allowed) {
2276
+ for (const key of allowed2) {
2275
2277
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
2276
2278
  result[key] = text2(input[key], `document.policy.${key}`, errors);
2277
2279
  }
@@ -2410,9 +2412,589 @@ var handleSignupControl = async (req, url, env, ctx) => {
2410
2412
  });
2411
2413
  };
2412
2414
 
2413
- // src/worker-routes-admission-grants.ts
2415
+ // src/worker-routes-member-content.ts
2414
2416
  var import_db4 = require("@odla-ai/db");
2417
+
2418
+ // src/member-content-canonical.ts
2419
+ var encoder2 = new TextEncoder();
2420
+ function canonicalValue2(value) {
2421
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2422
+ return JSON.stringify(value);
2423
+ }
2424
+ if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
2425
+ if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
2426
+ const record3 = value;
2427
+ const entries = Object.keys(record3).sort().flatMap((key) => {
2428
+ const item = record3[key];
2429
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
2430
+ });
2431
+ return `{${entries.join(",")}}`;
2432
+ }
2433
+ function canonicalMemberContent(value) {
2434
+ return canonicalValue2(value);
2435
+ }
2436
+ async function memberContentDigest(document) {
2437
+ const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
2438
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2439
+ }
2440
+
2441
+ // src/member-content-validation-helpers.ts
2442
+ var ID3 = /^[a-z][a-z0-9-]{0,79}$/;
2443
+ var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2444
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2445
+ var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
2446
+ var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
2447
+ var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
2448
+ var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
2449
+ var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
2450
+ var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
2451
+ var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
2452
+ var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
2453
+ var MAX_PAGES = 100;
2454
+ var MAX_MARKDOWN2 = 65536;
2455
+ var MAX_TEXT = 240;
2456
+ var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2457
+ function unknownKeys2(value, allowed2, path, errors) {
2458
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2459
+ }
2460
+ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
2461
+ if (value === void 0) {
2462
+ if (required) errors.push(`${path} is required`);
2463
+ return void 0;
2464
+ }
2465
+ if (typeof value !== "string") {
2466
+ errors.push(`${path} must be a string`);
2467
+ return void 0;
2468
+ }
2469
+ const valueText = value.trim();
2470
+ if (required && !valueText) errors.push(`${path} is required`);
2471
+ if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2472
+ return valueText;
2473
+ }
2474
+ function offerValue(value, path, errors, tiers, audience) {
2475
+ const raw = record2(value);
2476
+ if (!raw) {
2477
+ errors.push(`${path} must be an object`);
2478
+ return void 0;
2479
+ }
2480
+ unknownKeys2(raw, OFFER_KEYS, path, errors);
2481
+ const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2482
+ if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2483
+ if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2484
+ if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2485
+ errors.push(`${path}.tierId must grant access to this page`);
2486
+ }
2487
+ const label = textValue(raw.label, `${path}.label`, errors);
2488
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2);
2489
+ const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
2490
+ return {
2491
+ tierId,
2492
+ ...label ? { label } : {},
2493
+ ...bodyMarkdown ? { bodyMarkdown } : {},
2494
+ ...ctaLabel ? { ctaLabel } : {}
2495
+ };
2496
+ }
2497
+
2498
+ // src/member-content-validation.ts
2499
+ function validateMemberContentDocument(input, options = {}) {
2500
+ const errors = [];
2501
+ const root = record2(input);
2502
+ if (!root) return { ok: false, errors: ["document must be an object"] };
2503
+ unknownKeys2(root, ROOT_KEYS3, "document", errors);
2504
+ if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
2505
+ if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
2506
+ if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
2507
+ const sourceRaw = record2(root.source);
2508
+ let source = { type: "chapter", appId: "invalid" };
2509
+ if (!sourceRaw) errors.push("document.source must be an object");
2510
+ else {
2511
+ unknownKeys2(sourceRaw, SOURCE_KEYS, "document.source", errors);
2512
+ const type = sourceRaw.type;
2513
+ const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
2514
+ if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
2515
+ if (appId && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
2516
+ if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2517
+ if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
2518
+ if (type === "parent" || type === "chapter") source = { type, appId };
2519
+ }
2520
+ let target;
2521
+ if (root.target !== void 0) {
2522
+ const targetRaw = record2(root.target);
2523
+ if (!targetRaw) errors.push("document.target must be an object");
2524
+ else {
2525
+ unknownKeys2(targetRaw, TARGET_KEYS, "document.target", errors);
2526
+ const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
2527
+ const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
2528
+ target = { chapterId, environment };
2529
+ }
2530
+ }
2531
+ if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
2532
+ if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
2533
+ if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
2534
+ const rawPages = Array.isArray(root.pages) ? root.pages : [];
2535
+ if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
2536
+ const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
2537
+ const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
2538
+ const ids = /* @__PURE__ */ new Set();
2539
+ const slugs = /* @__PURE__ */ new Set();
2540
+ const pages = [];
2541
+ for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2542
+ const path = `document.pages[${index}]`;
2543
+ const raw = record2(value);
2544
+ if (!raw) {
2545
+ errors.push(`${path} must be an object`);
2546
+ continue;
2547
+ }
2548
+ unknownKeys2(raw, PAGE_KEYS, path, errors);
2549
+ const id = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
2550
+ const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
2551
+ const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2552
+ if (id && !ID3.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2553
+ if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2554
+ if (ids.has(id) || knownParentIds.has(id)) errors.push(`${path}.id duplicates page "${id}"`);
2555
+ if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
2556
+ ids.add(id);
2557
+ slugs.add(slug);
2558
+ const navRaw = record2(raw.navigation);
2559
+ let navigation = { section: "General", order: index };
2560
+ if (!navRaw) errors.push(`${path}.navigation must be an object`);
2561
+ else {
2562
+ unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
2563
+ const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
2564
+ const order = navRaw.order;
2565
+ if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
2566
+ errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
2567
+ }
2568
+ const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
2569
+ navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
2570
+ }
2571
+ const audienceRaw = record2(raw.audience);
2572
+ let audience = { kind: "all-members" };
2573
+ if (!audienceRaw) errors.push(`${path}.audience must be an object`);
2574
+ else {
2575
+ unknownKeys2(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
2576
+ if (audienceRaw.kind === "all-members") {
2577
+ if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
2578
+ } else if (audienceRaw.kind === "tiers") {
2579
+ if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
2580
+ errors.push(`${path}.audience.tierIds must be a non-empty array`);
2581
+ }
2582
+ const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
2583
+ const normalized = [];
2584
+ for (const [tierIndex, value2] of values.entries()) {
2585
+ const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
2586
+ if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
2587
+ if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
2588
+ normalized.push(tier);
2589
+ }
2590
+ audience = { kind: "tiers", tierIds: normalized };
2591
+ } else errors.push(`${path}.audience.kind must be all-members or tiers`);
2592
+ }
2593
+ const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
2594
+ const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
2595
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
2596
+ let upsell;
2597
+ if (raw.upsell !== void 0) {
2598
+ const upsellRaw = record2(raw.upsell);
2599
+ if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
2600
+ else {
2601
+ unknownKeys2(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
2602
+ const owner = upsellRaw.owner;
2603
+ if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
2604
+ if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
2605
+ if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
2606
+ if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
2607
+ const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
2608
+ if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
2609
+ else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
2610
+ }
2611
+ }
2612
+ pages.push({ id, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
2613
+ }
2614
+ const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
2615
+ for (const [index, page] of pages.entries()) {
2616
+ const parentId = page.navigation.parentId;
2617
+ if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
2618
+ if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
2619
+ }
2620
+ for (const page of pages) {
2621
+ const seen = /* @__PURE__ */ new Set([page.id]);
2622
+ let parentId = page.navigation.parentId;
2623
+ while (parentId) {
2624
+ if (seen.has(parentId)) {
2625
+ errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
2626
+ break;
2627
+ }
2628
+ seen.add(parentId);
2629
+ parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
2630
+ }
2631
+ }
2632
+ let upsellOverrides;
2633
+ if (root.upsellOverrides !== void 0) {
2634
+ if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
2635
+ if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
2636
+ const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
2637
+ const seen = /* @__PURE__ */ new Set();
2638
+ upsellOverrides = [];
2639
+ for (const [index, value] of values.entries()) {
2640
+ const path = `document.upsellOverrides[${index}]`;
2641
+ const raw = record2(value);
2642
+ if (!raw) {
2643
+ errors.push(`${path} must be an object`);
2644
+ continue;
2645
+ }
2646
+ unknownKeys2(raw, OVERRIDE_KEYS, path, errors);
2647
+ const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
2648
+ if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
2649
+ seen.add(pageId);
2650
+ const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
2651
+ if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
2652
+ else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
2653
+ const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
2654
+ if (offer) upsellOverrides.push({ pageId, offer });
2655
+ }
2656
+ }
2657
+ if (errors.length) return { ok: false, errors };
2658
+ return { ok: true, value: {
2659
+ schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
2660
+ revision: Number(root.revision),
2661
+ generatedAt: Number(root.generatedAt),
2662
+ source,
2663
+ ...target ? { target } : {},
2664
+ pages,
2665
+ ...upsellOverrides ? { upsellOverrides } : {}
2666
+ } };
2667
+ }
2668
+
2669
+ // src/member-content.ts
2670
+ var MEMBER_CONTENT_SCHEMA_VERSION = 1;
2671
+ var MEMBER_CONTENT_PATH = "/api/member-content";
2672
+ var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
2673
+ 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);
2674
+ function composeMemberContent(parent, chapter) {
2675
+ const pages = [];
2676
+ const parentPages = /* @__PURE__ */ new Map();
2677
+ for (const page of parent?.pages ?? []) {
2678
+ const effective = { ...page, source: parent.source };
2679
+ parentPages.set(page.id, effective);
2680
+ pages.push(effective);
2681
+ }
2682
+ for (const override of chapter?.upsellOverrides ?? []) {
2683
+ const page = parentPages.get(override.pageId);
2684
+ if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
2685
+ }
2686
+ for (const page of chapter?.pages ?? []) {
2687
+ if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
2688
+ throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
2689
+ }
2690
+ pages.push({ ...page, source: chapter.source });
2691
+ }
2692
+ return {
2693
+ pages: pages.sort(pageOrder),
2694
+ parentRevision: parent?.revision ?? null,
2695
+ chapterRevision: chapter?.revision ?? null
2696
+ };
2697
+ }
2698
+ var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
2699
+ function projectMemberContent(content, viewer, tiers) {
2700
+ const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
2701
+ return {
2702
+ viewer: { ...viewer },
2703
+ revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
2704
+ pages: content.pages.map((page) => {
2705
+ const canRead = allowed(page, viewer);
2706
+ const offer = !canRead ? page.upsell?.offer : void 0;
2707
+ const tier = offer ? authority.get(offer.tierId) : void 0;
2708
+ const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
2709
+ return {
2710
+ id: page.id,
2711
+ slug: page.slug,
2712
+ title: page.title,
2713
+ ...page.summary ? { summary: page.summary } : {},
2714
+ navigation: page.navigation,
2715
+ audience: page.audience,
2716
+ source: page.source,
2717
+ access: canRead ? "allowed" : "locked",
2718
+ ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
2719
+ ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
2720
+ ...upsell ? { upsell } : {}
2721
+ };
2722
+ })
2723
+ };
2724
+ }
2725
+
2726
+ // src/member-content-store.ts
2727
+ var control = (row) => row && row.document && typeof row.document === "object" ? row : null;
2728
+ var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
2729
+ async function exists(db, ns, where) {
2730
+ if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
2731
+ const rows4 = (await db.query({ [ns]: {} }))[ns] ?? [];
2732
+ return rows4.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
2733
+ }
2734
+ async function loadActiveMemberContentControl(db, targetId, environment) {
2735
+ const where = { targetId, environment, active: true };
2736
+ if (!await exists(db, "memberContentControls", where)) return null;
2737
+ const rows4 = (await db.query({ memberContentControls: { $: {
2738
+ where,
2739
+ order: { revision: "desc" },
2740
+ limit: 1
2741
+ } } })).memberContentControls;
2742
+ return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
2743
+ }
2744
+ async function loadLatestMemberContentControl(db, targetId, environment) {
2745
+ const where = { targetId, environment };
2746
+ if (!await exists(db, "memberContentControls", where)) return null;
2747
+ const rows4 = (await db.query({ memberContentControls: { $: {
2748
+ where,
2749
+ order: { revision: "desc" },
2750
+ limit: 1
2751
+ } } })).memberContentControls;
2752
+ return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
2753
+ }
2754
+ async function loadLocalMemberContent(db, chapterId, environment) {
2755
+ const id = `member-content-local:${environment}`;
2756
+ const rows4 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
2757
+ return local(Array.isArray(rows4) ? rows4[0] : void 0);
2758
+ }
2759
+ async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
2760
+ const { document, digest } = envelope;
2761
+ const target = document.target;
2762
+ const id = `member-content-control:${target.environment}:${document.revision}`;
2763
+ const ops = [];
2764
+ if (previous?.active) {
2765
+ ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
2766
+ }
2767
+ ops.push({ t: "update", ns: "memberContentControls", id, attrs: {
2768
+ id,
2769
+ sourceId: document.source.appId,
2770
+ targetId: target.chapterId,
2771
+ environment: target.environment,
2772
+ revision: document.revision,
2773
+ digest,
2774
+ document,
2775
+ active: true,
2776
+ receivedAt
2777
+ } });
2778
+ await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
2779
+ }
2780
+ async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
2781
+ const id = `member-content-local:${environment}`;
2782
+ const current = await loadLocalMemberContent(db, document.source.appId, environment);
2783
+ if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
2784
+ const row = {
2785
+ id,
2786
+ chapterId: document.source.appId,
2787
+ environment,
2788
+ revision: document.revision,
2789
+ document,
2790
+ updatedAt,
2791
+ ...updatedBy ? { updatedBy } : {}
2792
+ };
2793
+ await db.transact([{ t: "update", ns: "memberContentLocal", id, attrs: row }], {
2794
+ mutationId: `member-content-local:${environment}:${document.revision}`,
2795
+ guards: current ? [{ ns: "memberContentLocal", id, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id, exists: false }]
2796
+ });
2797
+ return row;
2798
+ }
2799
+
2800
+ // src/worker-routes-member-content.ts
2801
+ var ADMIN_PATH = "/api/admin/member-content";
2802
+ var BODY_CAP = 1e6;
2415
2803
  var reject2 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2804
+ async function groupAndTiers(db, chapterId) {
2805
+ const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
2806
+ const group = Array.isArray(groups) ? groups[0] : void 0;
2807
+ if (!group) return { group: null, tiers: [] };
2808
+ return { group, tiers: await loadTiers(db, group) };
2809
+ }
2810
+ async function memberTierId(db, chapterId, email) {
2811
+ if (!email) return null;
2812
+ let rows4 = (await db.query({ applications: { $: {
2813
+ where: { emailNormalized: normalizeAdmissionEmail(email) },
2814
+ limit: 20
2815
+ } } })).applications;
2816
+ if (!rows4?.length) rows4 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
2817
+ 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;
2818
+ return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
2819
+ }
2820
+ async function activeDocuments(db, chapterId, environment) {
2821
+ const [parent, chapter] = await Promise.all([
2822
+ loadActiveMemberContentControl(db, chapterId, environment),
2823
+ loadLocalMemberContent(db, chapterId, environment)
2824
+ ]);
2825
+ return { parent, chapter };
2826
+ }
2827
+ var handleMemberContentControl = async (req, url, env, ctx) => {
2828
+ if (req.method !== "POST" || url.pathname !== MEMBER_CONTENT_CONTROL_PATH) return null;
2829
+ const settings = ctx.chapter.memberContent?.parent;
2830
+ if (!settings) return json({ error: "not found" }, 404);
2831
+ const db = ctx.makeDb(env);
2832
+ const secret = await getVaultSecret(db, settings.secretName);
2833
+ if (!secret) return json({ error: "unauthorized" }, 401);
2834
+ const verified = await (0, import_db4.verifyFederatedRequest)(req, { secret, senders: [settings.sourceId] });
2835
+ if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2836
+ let input;
2837
+ try {
2838
+ input = JSON.parse(verified.body);
2839
+ } catch {
2840
+ return reject2("invalid_document", 400, ["request body must be JSON"]);
2841
+ }
2842
+ const digest = typeof input.digest === "string" ? input.digest : "";
2843
+ const initial = validateMemberContentDocument(input.document, {
2844
+ sourceType: "parent"
2845
+ });
2846
+ if (!initial.ok) return reject2("invalid_document", 400, initial.errors);
2847
+ const document = initial.value;
2848
+ if (document.source.appId !== settings.sourceId) return reject2("wrong_source", 403);
2849
+ if (document.target?.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
2850
+ if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
2851
+ const { tiers } = await groupAndTiers(db, ctx.chapter.id);
2852
+ const checked = validateMemberContentDocument(document, {
2853
+ sourceType: "parent",
2854
+ sourceAppId: settings.sourceId,
2855
+ tiers
2856
+ });
2857
+ if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
2858
+ const local2 = await loadLocalMemberContent(db, ctx.chapter.id, env.ODLA_ENV);
2859
+ if (local2) {
2860
+ const localCheck = validateMemberContentDocument(local2.document, {
2861
+ sourceType: "chapter",
2862
+ sourceAppId: ctx.chapter.id,
2863
+ tiers,
2864
+ parentPages: document.pages
2865
+ });
2866
+ if (!localCheck.ok) {
2867
+ return reject2("invalid_document", 409, localCheck.errors.map((error) => `existing chapter content: ${error}`));
2868
+ }
2869
+ }
2870
+ if (!digest || await memberContentDigest(document) !== digest) return reject2("invalid_digest", 400);
2871
+ const latest = await loadLatestMemberContentControl(db, ctx.chapter.id, env.ODLA_ENV);
2872
+ if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
2873
+ if (latest && document.revision === latest.revision) {
2874
+ if (latest.digest !== digest) return reject2("conflicting_revision", 409);
2875
+ return json({
2876
+ ok: true,
2877
+ target: document.target,
2878
+ revision: latest.revision,
2879
+ digest: latest.digest,
2880
+ status: "replayed",
2881
+ acknowledgedAt: latest.receivedAt
2882
+ });
2883
+ }
2884
+ const receivedAt = Date.now();
2885
+ try {
2886
+ await materializeMemberContentControl(db, { digest, document }, latest, receivedAt);
2887
+ } catch {
2888
+ return reject2("materialization_failed", 503);
2889
+ }
2890
+ return json({
2891
+ ok: true,
2892
+ target: document.target,
2893
+ revision: document.revision,
2894
+ digest,
2895
+ status: "accepted",
2896
+ acknowledgedAt: receivedAt
2897
+ });
2898
+ };
2899
+ var handleMemberContent = async (req, url, env, ctx) => {
2900
+ if (req.method !== "GET" || url.pathname !== MEMBER_CONTENT_PATH) return null;
2901
+ if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
2902
+ const user = await ctx.verifyUser(req, env);
2903
+ if (!user) return json({ error: "unauthorized" }, 401);
2904
+ const db = ctx.makeDb(env);
2905
+ const role = await ctx.roleFor(db, user);
2906
+ const admin = await ctx.isAdmin(db, user);
2907
+ const roleIndex = ctx.chapter.auth.ladder.indexOf(role);
2908
+ const memberIndex = Math.max(0, ctx.chapter.auth.ladder.length - 2);
2909
+ if (!admin && roleIndex < memberIndex) return json({ error: "member access required" }, 403);
2910
+ const [{ parent, chapter }, { group, tiers }, tierId] = await Promise.all([
2911
+ activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
2912
+ groupAndTiers(db, ctx.chapter.id),
2913
+ memberTierId(db, ctx.chapter.id, user.email)
2914
+ ]);
2915
+ const effective = composeMemberContent(parent?.document, chapter?.document);
2916
+ let offerable = tiers;
2917
+ if (group) {
2918
+ const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
2919
+ offerable = await loadOfferableTiers(db, group, Boolean(stripeSecret));
2920
+ }
2921
+ return json(projectMemberContent(effective, { tierId, admin }, offerable));
2922
+ };
2923
+ var handleAdminMemberContent = async (req, url, env, ctx) => {
2924
+ if (url.pathname !== ADMIN_PATH) return null;
2925
+ if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
2926
+ const user = await ctx.verifyUser(req, env);
2927
+ if (!user) return json({ error: "unauthorized" }, 401);
2928
+ const db = ctx.makeDb(env);
2929
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
2930
+ const [{ parent, chapter }, { tiers }] = await Promise.all([
2931
+ activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
2932
+ groupAndTiers(db, ctx.chapter.id)
2933
+ ]);
2934
+ if (req.method === "GET") {
2935
+ const effective = composeMemberContent(parent?.document, chapter?.document);
2936
+ return json({
2937
+ parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
2938
+ chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
2939
+ effective,
2940
+ tiers: tiers.map(({ id, name, priceCents, active }) => ({ id, name, priceCents, active })),
2941
+ localAuthoring: ctx.chapter.memberContent.localAuthoring
2942
+ });
2943
+ }
2944
+ if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
2945
+ if (!ctx.chapter.memberContent.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
2946
+ const raw = await req.text();
2947
+ if (raw.length > BODY_CAP) return json({ error: "request body too large" }, 413);
2948
+ let input;
2949
+ try {
2950
+ input = JSON.parse(raw);
2951
+ } catch {
2952
+ return json({ error: "invalid JSON body" }, 400);
2953
+ }
2954
+ const expectedRevision = Number(input.expectedRevision);
2955
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 0) {
2956
+ return json({ error: "expectedRevision must be a non-negative integer" }, 400);
2957
+ }
2958
+ const now = Date.now();
2959
+ const documentInput = withLocalAuthority(input.document, expectedRevision + 1, now, ctx.chapter.id);
2960
+ const checked = validateMemberContentDocument(documentInput, {
2961
+ sourceType: "chapter",
2962
+ sourceAppId: ctx.chapter.id,
2963
+ tiers,
2964
+ parentPages: parent?.document.pages ?? []
2965
+ });
2966
+ if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
2967
+ try {
2968
+ const saved = await saveLocalMemberContent(
2969
+ db,
2970
+ checked.value,
2971
+ env.ODLA_ENV,
2972
+ expectedRevision,
2973
+ now,
2974
+ user.email ?? user.userId
2975
+ );
2976
+ return json({ ok: true, revision: saved.revision, document: saved.document });
2977
+ } catch (error) {
2978
+ if (error instanceof Error && error.message === "member_content_revision_conflict") {
2979
+ return json({ error: "member content changed; reload before saving" }, 409);
2980
+ }
2981
+ return json({ error: "member content could not be saved" }, 503);
2982
+ }
2983
+ };
2984
+ function withLocalAuthority(value, revision, generatedAt, appId) {
2985
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
2986
+ return {
2987
+ ...value,
2988
+ schemaVersion: 1,
2989
+ revision,
2990
+ generatedAt,
2991
+ source: { type: "chapter", appId }
2992
+ };
2993
+ }
2994
+
2995
+ // src/worker-routes-admission-grants.ts
2996
+ var import_db5 = require("@odla-ai/db");
2997
+ var reject3 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2416
2998
  var handleAdmissionGrants = async (req, url, env, ctx) => {
2417
2999
  if (req.method !== "POST" || url.pathname !== ADMISSION_GRANTS_PATH) return null;
2418
3000
  const authority = ctx.chapter.signupControl;
@@ -2420,26 +3002,26 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
2420
3002
  const db = ctx.makeDb(env);
2421
3003
  const secret = await getVaultSecret(db, authority.secretName);
2422
3004
  if (!secret) return json({ error: "unauthorized" }, 401);
2423
- const verified = await (0, import_db4.verifyFederatedRequest)(req, { secret, senders: [authority.sourceId] });
3005
+ const verified = await (0, import_db5.verifyFederatedRequest)(req, { secret, senders: [authority.sourceId] });
2424
3006
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2425
3007
  let input;
2426
3008
  try {
2427
3009
  input = JSON.parse(verified.body);
2428
3010
  } catch {
2429
- return reject2("invalid_document", 400, ["request body must be JSON"]);
3011
+ return reject3("invalid_document", 400, ["request body must be JSON"]);
2430
3012
  }
2431
3013
  const digest = typeof input.digest === "string" ? input.digest : "";
2432
3014
  const checked = validateAdmissionGrantDocument(input.document);
2433
- if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
3015
+ if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
2434
3016
  const document = checked.value;
2435
- if (document.source.appId !== authority.sourceId) return reject2("wrong_source", 403);
2436
- if (document.target.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
2437
- if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
2438
- if (!digest || await signupControlDigest(document) !== digest) return reject2("invalid_digest", 400);
3017
+ if (document.source.appId !== authority.sourceId) return reject3("wrong_source", 403);
3018
+ if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
3019
+ if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
3020
+ if (!digest || await signupControlDigest(document) !== digest) return reject3("invalid_digest", 400);
2439
3021
  const latest = await loadLatestAdmissionRevision(db, authority.sourceId, ctx.chapter.id, env.ODLA_ENV);
2440
- if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
3022
+ if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
2441
3023
  if (latest && document.revision === latest.revision) {
2442
- if (latest.digest !== digest) return reject2("conflicting_revision", 409);
3024
+ if (latest.digest !== digest) return reject3("conflicting_revision", 409);
2443
3025
  return json({
2444
3026
  ok: true,
2445
3027
  target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
@@ -2453,7 +3035,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
2453
3035
  try {
2454
3036
  await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
2455
3037
  } catch {
2456
- return reject2("materialization_failed", 503);
3038
+ return reject3("materialization_failed", 503);
2457
3039
  }
2458
3040
  return json({
2459
3041
  ok: true,
@@ -3281,8 +3863,8 @@ async function startSubscription(req, env, ctx) {
3281
3863
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
3282
3864
  if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
3283
3865
  if (typeof app.giftOfferId === "string") {
3284
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3285
- const offer = findGiftOffer(control?.document.giftOffers, app.giftOfferId, Date.now());
3866
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3867
+ const offer = findGiftOffer(control2?.document.giftOffers, app.giftOfferId, Date.now());
3286
3868
  if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
3287
3869
  return json({ error: "gift offer is unavailable" }, 409);
3288
3870
  }
@@ -3441,14 +4023,14 @@ var handleGifts = async (req, url, env, ctx) => {
3441
4023
  if (req.method === "GET" && url.pathname === "/api/gifts") {
3442
4024
  const user = await userFor(req, env, ctx);
3443
4025
  if (!user) return json({ error: "unauthorized" }, 401);
3444
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4026
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3445
4027
  const found = await db.query({ giftClaims: { $: { where: {
3446
4028
  purchaserEmailNormalized: normalizeAdmissionEmail(user.email),
3447
4029
  chapterId: ctx.chapter.id
3448
4030
  }, limit: 200 } } });
3449
4031
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3450
4032
  return json({
3451
- offers: publicGiftOffers(control?.document.giftOffers, Date.now()),
4033
+ offers: publicGiftOffers(control2?.document.giftOffers, Date.now()),
3452
4034
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
3453
4035
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
3454
4036
  });
@@ -3464,8 +4046,8 @@ var handleGifts = async (req, url, env, ctx) => {
3464
4046
  }
3465
4047
  const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
3466
4048
  if (!/^[A-Za-z0-9_-]{16,128}$/.test(purchaseId)) return json({ error: "purchaseId is invalid" }, 400);
3467
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3468
- const offer = findGiftOffer(control?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
4049
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4050
+ const offer = findGiftOffer(control2?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
3469
4051
  if (!offer) return json({ error: "gift offer is unavailable" }, 409);
3470
4052
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3471
4053
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
@@ -4386,7 +4968,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
4386
4968
 
4387
4969
  // src/worker-routes-network.ts
4388
4970
  var import_crm6 = require("@odla-ai/crm");
4389
- var import_db6 = require("@odla-ai/db");
4971
+ var import_db7 = require("@odla-ai/db");
4390
4972
 
4391
4973
  // src/worker-network-fetch.ts
4392
4974
  function targetFetcher(env, target) {
@@ -4404,8 +4986,8 @@ function fetchNetworkTarget(env, target, destination, init) {
4404
4986
 
4405
4987
  // src/worker-network-push.ts
4406
4988
  var import_crm5 = require("@odla-ai/crm");
4407
- var import_db5 = require("@odla-ai/db");
4408
- async function pushNetworkRecord(db, env, ctx, target, record2) {
4989
+ var import_db6 = require("@odla-ai/db");
4990
+ async function pushNetworkRecord(db, env, ctx, target, record3) {
4409
4991
  const secret = await getVaultSecret(db, target.secretName);
4410
4992
  const crmDeps3 = {
4411
4993
  crm: ctx.chapter.crm,
@@ -4416,7 +4998,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4416
4998
  if (!secret) {
4417
4999
  const error = `vault secret "${target.secretName}" is missing`;
4418
5000
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
4419
- recordId: record2.id,
5001
+ recordId: record3.id,
4420
5002
  targetId: target.id,
4421
5003
  status: "failed",
4422
5004
  payloadVersion: 2,
@@ -4426,7 +5008,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4426
5008
  }
4427
5009
  let payload;
4428
5010
  try {
4429
- payload = sharedRecordFromCrm(ctx.chapter.crm, record2, target, ctx.chapter.id);
5011
+ payload = sharedRecordFromCrm(ctx.chapter.crm, record3, target, ctx.chapter.id);
4430
5012
  } catch (err) {
4431
5013
  return {
4432
5014
  id: target.id,
@@ -4438,7 +5020,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4438
5020
  const payloadBody = JSON.stringify(payload);
4439
5021
  const destination = new URL("/api/network/shared", target.url);
4440
5022
  try {
4441
- const signed = await (0, import_db5.signFederatedRequest)({
5023
+ const signed = await (0, import_db6.signFederatedRequest)({
4442
5024
  secret,
4443
5025
  sender: ctx.chapter.id,
4444
5026
  method: "POST",
@@ -4455,7 +5037,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4455
5037
  if (!res.ok) {
4456
5038
  const error = responseBody.error ?? "follower rejected the record";
4457
5039
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
4458
- recordId: record2.id,
5040
+ recordId: record3.id,
4459
5041
  targetId: target.id,
4460
5042
  status: "failed",
4461
5043
  payloadVersion: 2,
@@ -4470,7 +5052,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4470
5052
  };
4471
5053
  }
4472
5054
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
4473
- recordId: record2.id,
5055
+ recordId: record3.id,
4474
5056
  targetId: target.id,
4475
5057
  status: "delivered",
4476
5058
  payloadVersion: 2,
@@ -4486,7 +5068,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4486
5068
  } catch (err) {
4487
5069
  const error = err instanceof Error ? err.message : "delivery failed";
4488
5070
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
4489
- recordId: record2.id,
5071
+ recordId: record3.id,
4490
5072
  targetId: target.id,
4491
5073
  status: "failed",
4492
5074
  payloadVersion: 2,
@@ -4523,7 +5105,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
4523
5105
  const db = ctx.makeDb(env);
4524
5106
  const secret = await getVaultSecret(db, "network_share_secret");
4525
5107
  if (!secret) return json({ error: "unauthorized" }, 401);
4526
- const verified = await (0, import_db6.verifyFederatedRequest)(req, { secret });
5108
+ const verified = await (0, import_db7.verifyFederatedRequest)(req, { secret });
4527
5109
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
4528
5110
  const types = await Promise.all(
4529
5111
  Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm6.typeSummary)({ crm: ctx.chapter.crm, db }, type))
@@ -4541,7 +5123,7 @@ async function snapshotOne(db, env, ctx, target) {
4541
5123
  if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
4542
5124
  const destination = new URL("/api/network/snapshot", target.url);
4543
5125
  try {
4544
- const headers = await (0, import_db6.signFederatedRequest)({
5126
+ const headers = await (0, import_db7.signFederatedRequest)({
4545
5127
  secret,
4546
5128
  sender: ctx.chapter.id,
4547
5129
  method: "GET",
@@ -4624,17 +5206,17 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
4624
5206
  if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
4625
5207
  const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
4626
5208
  if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
4627
- const record2 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
4628
- if (!record2) return json({ error: "record not found" }, 404);
5209
+ const record3 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
5210
+ if (!record3) return json({ error: "record not found" }, 404);
4629
5211
  const results = await Promise.all(
4630
- targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record2))
5212
+ targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record3))
4631
5213
  );
4632
5214
  return json({ ok: results.every((result) => result.ok), results });
4633
5215
  };
4634
5216
 
4635
5217
  // src/worker-routes-network-records.ts
4636
5218
  var import_crm7 = require("@odla-ai/crm");
4637
- var import_db7 = require("@odla-ai/db");
5219
+ var import_db8 = require("@odla-ai/db");
4638
5220
  var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
4639
5221
  var MAX_PAGE = 50;
4640
5222
  var MAX_OFFSET = 1e4;
@@ -4652,18 +5234,18 @@ function integer(value, fallback, min, max) {
4652
5234
  const parsed = Number(value);
4653
5235
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
4654
5236
  }
4655
- function projectRecord(record2, allowed) {
5237
+ function projectRecord(record3, allowed2) {
4656
5238
  const fields = Object.fromEntries(
4657
- allowed.flatMap((name) => Object.prototype.hasOwnProperty.call(record2.fields, name) ? [[name, record2.fields[name]]] : [])
5239
+ allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record3.fields, name) ? [[name, record3.fields[name]]] : [])
4658
5240
  );
4659
5241
  return {
4660
- id: record2.id,
4661
- type: record2.type,
4662
- name: record2.name,
4663
- ...record2.stage ? { stage: record2.stage } : {},
5242
+ id: record3.id,
5243
+ type: record3.type,
5244
+ name: record3.name,
5245
+ ...record3.stage ? { stage: record3.stage } : {},
4664
5246
  fields,
4665
- createdAt: record2.createdAt,
4666
- updatedAt: record2.updatedAt
5247
+ createdAt: record3.createdAt,
5248
+ updatedAt: record3.updatedAt
4667
5249
  };
4668
5250
  }
4669
5251
  var handleNetworkRecords = async (req, url, env, ctx) => {
@@ -4672,15 +5254,15 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
4672
5254
  const secret = await getVaultSecret(db, "network_share_secret");
4673
5255
  const readers = ctx.chapter.network.readers;
4674
5256
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
4675
- const verified = await (0, import_db7.verifyFederatedRequest)(req, {
5257
+ const verified = await (0, import_db8.verifyFederatedRequest)(req, {
4676
5258
  secret,
4677
5259
  senders: readers.map((reader2) => reader2.id)
4678
5260
  });
4679
5261
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
4680
5262
  const reader = readers.find((candidate) => candidate.id === verified.sender);
4681
5263
  const type = url.searchParams.get("type") ?? "";
4682
- const allowed = reader?.fields[type];
4683
- if (!IDENTIFIER.test(type) || !allowed) return json({ error: "record type is not shared with this reader" }, 403);
5264
+ const allowed2 = reader?.fields[type];
5265
+ if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
4684
5266
  const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
4685
5267
  const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
4686
5268
  const search = (url.searchParams.get("search") ?? "").trim();
@@ -4701,7 +5283,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
4701
5283
  version: 1,
4702
5284
  site: { id: ctx.chapter.id, name: ctx.chapter.name },
4703
5285
  type,
4704
- records: page.records.map((record2) => projectRecord(record2, allowed)),
5286
+ records: page.records.map((record3) => projectRecord(record3, allowed2)),
4705
5287
  total: page.total,
4706
5288
  limit: page.limit,
4707
5289
  offset: page.offset
@@ -4717,7 +5299,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
4717
5299
  const destination = new URL("/api/network/records", target.url);
4718
5300
  destination.search = query.toString();
4719
5301
  try {
4720
- const headers = await (0, import_db7.signFederatedRequest)({
5302
+ const headers = await (0, import_db8.signFederatedRequest)({
4721
5303
  secret,
4722
5304
  sender: ctx.chapter.id,
4723
5305
  method: "GET",
@@ -4852,7 +5434,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
4852
5434
 
4853
5435
  // src/worker-routes-network-shared-notes.ts
4854
5436
  var import_crm8 = require("@odla-ai/crm");
4855
- var import_db8 = require("@odla-ai/db");
5437
+ var import_db9 = require("@odla-ai/db");
4856
5438
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
4857
5439
  var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
4858
5440
  var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
@@ -4911,7 +5493,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
4911
5493
  const secret = await getVaultSecret(db, "network_share_secret");
4912
5494
  const readers = ctx.chapter.network.readers;
4913
5495
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
4914
- const verified = await (0, import_db8.verifyFederatedRequest)(req, {
5496
+ const verified = await (0, import_db9.verifyFederatedRequest)(req, {
4915
5497
  secret,
4916
5498
  senders: readers.map((reader2) => reader2.id)
4917
5499
  });
@@ -4922,8 +5504,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
4922
5504
  if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
4923
5505
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
4924
5506
  }
4925
- const record2 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
4926
- if (!record2 || record2.type !== recordType) return json({ error: "record not found" }, 404);
5507
+ const record3 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
5508
+ if (!record3 || record3.type !== recordType) return json({ error: "record not found" }, 404);
4927
5509
  if (req.method === "GET") {
4928
5510
  return json({
4929
5511
  version: 1,
@@ -5008,7 +5590,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5008
5590
  const destination = new URL("/api/network/shared-notes", target.url);
5009
5591
  destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
5010
5592
  try {
5011
- const headers = await (0, import_db8.signFederatedRequest)({
5593
+ const headers = await (0, import_db9.signFederatedRequest)({
5012
5594
  secret,
5013
5595
  sender: ctx.chapter.id,
5014
5596
  method: req.method,
@@ -5077,15 +5659,15 @@ var handleFormation = async (req, url, env, ctx) => {
5077
5659
  if (submissionId && !SUBMISSION.test(submissionId)) {
5078
5660
  return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
5079
5661
  }
5080
- const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
5081
- const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
5662
+ const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
5663
+ const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed2.has(field));
5082
5664
  if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
5083
5665
  for (const field of formation.required) {
5084
5666
  const value = body[field];
5085
5667
  if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
5086
5668
  }
5087
5669
  const input = {};
5088
- for (const field of allowed) {
5670
+ for (const field of allowed2) {
5089
5671
  const value = body[field];
5090
5672
  if (value === void 0) continue;
5091
5673
  const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
@@ -5129,11 +5711,11 @@ var handleFormation = async (req, url, env, ctx) => {
5129
5711
  };
5130
5712
 
5131
5713
  // src/signup-control-delivery.ts
5132
- var import_db9 = require("@odla-ai/db");
5714
+ var import_db10 = require("@odla-ai/db");
5133
5715
  async function deliverSignupControl(options) {
5134
5716
  const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
5135
5717
  const body = JSON.stringify(options.envelope);
5136
- const signed = await (0, import_db9.signFederatedRequest)({
5718
+ const signed = await (0, import_db10.signFederatedRequest)({
5137
5719
  secret: options.secret,
5138
5720
  sender: options.sender,
5139
5721
  method: "POST",
@@ -5153,12 +5735,37 @@ async function deliverSignupControl(options) {
5153
5735
  return result;
5154
5736
  }
5155
5737
 
5738
+ // src/member-content-delivery.ts
5739
+ var import_db11 = require("@odla-ai/db");
5740
+ async function deliverMemberContent(options) {
5741
+ const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
5742
+ const body = JSON.stringify(options.envelope);
5743
+ const signed = await (0, import_db11.signFederatedRequest)({
5744
+ secret: options.secret,
5745
+ sender: options.sender,
5746
+ method: "POST",
5747
+ url: destination,
5748
+ body
5749
+ });
5750
+ const response = await (options.fetch ?? globalThis.fetch)(destination, {
5751
+ method: "POST",
5752
+ headers: { ...signed, "content-type": "application/json" },
5753
+ body
5754
+ });
5755
+ const result = await response.json();
5756
+ if (!response.ok || !("ok" in result) || result.ok !== true) {
5757
+ const failure = result;
5758
+ throw new Error(`member content rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
5759
+ }
5760
+ return result;
5761
+ }
5762
+
5156
5763
  // src/admission-grants-delivery.ts
5157
- var import_db10 = require("@odla-ai/db");
5764
+ var import_db12 = require("@odla-ai/db");
5158
5765
  async function deliverAdmissionGrants(options) {
5159
5766
  const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
5160
5767
  const body = JSON.stringify(options.envelope);
5161
- const signed = await (0, import_db10.signFederatedRequest)({
5768
+ const signed = await (0, import_db12.signFederatedRequest)({
5162
5769
  secret: options.secret,
5163
5770
  sender: options.sender,
5164
5771
  method: "POST",
@@ -5317,12 +5924,14 @@ var BUILTIN_ROUTES = [
5317
5924
  handleCrm,
5318
5925
  handleNetworkShared,
5319
5926
  handleSignupControl,
5927
+ handleMemberContentControl,
5320
5928
  handleAdmissionGrants,
5321
5929
  handleNetworkSnapshot,
5322
5930
  handleNetworkRecords,
5323
5931
  handleNetworkSharedNotes,
5324
5932
  handleFormation,
5325
5933
  handleMember,
5934
+ handleMemberContent,
5326
5935
  handleGifts,
5327
5936
  handleSchedule,
5328
5937
  handlePayments,
@@ -5338,6 +5947,7 @@ var BUILTIN_ROUTES = [
5338
5947
  // Aggregation
5339
5948
  handleAdminDashboard,
5340
5949
  handleAdminBilling,
5950
+ handleAdminMemberContent,
5341
5951
  // Lifecycle actions
5342
5952
  handleAdminMeetingReschedule,
5343
5953
  handleAdminMeetingCancel,