@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.
@@ -179,9 +179,9 @@ function canonicalValue(value) {
179
179
  }
180
180
  if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
181
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];
182
+ const record3 = value;
183
+ const entries = Object.keys(record3).sort().flatMap((key) => {
184
+ const item = record3[key];
185
185
  return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
186
186
  });
187
187
  return `{${entries.join(",")}}`;
@@ -431,14 +431,14 @@ function hasDisclaimerAck(fields) {
431
431
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
432
432
  function applicantProfile(chapter, fields) {
433
433
  const app = chapter.application;
434
- const allowed = (f) => app.profileFields.includes(f);
434
+ const allowed2 = (f) => app.profileFields.includes(f);
435
435
  const profile = {};
436
436
  for (const f of [...app.required, ...app.optional]) {
437
- if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
437
+ if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
438
438
  const v = fields[f];
439
439
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
440
440
  }
441
- if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
441
+ if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
442
442
  return Object.keys(profile).length > 0 ? profile : void 0;
443
443
  }
444
444
  async function submitApplication(db, chapter, fields, opts) {
@@ -731,8 +731,8 @@ function validateGiftOfferCatalog(input, tierIds) {
731
731
  errors.push(`${path} must be an object`);
732
732
  continue;
733
733
  }
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`);
734
+ const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
735
+ for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
736
736
  const id = text(item.id, `${path}.id`, errors, 80);
737
737
  if (id && !ID2.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
738
738
  if (ids.has(id)) errors.push(`${path}.id duplicates "${id}"`);
@@ -829,8 +829,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
829
829
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
830
830
  const page = Array.isArray(data) ? data : [];
831
831
  for (const u of page) {
832
- const record2 = toRecord(u);
833
- if (record2) out.push(record2);
832
+ const record3 = toRecord(u);
833
+ if (record3) out.push(record3);
834
834
  }
835
835
  if (page.length < PAGE) break;
836
836
  }
@@ -892,34 +892,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
892
892
  if (readable && raw.length <= 64) return raw;
893
893
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
894
894
  }
895
- function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
896
- if ("input" in record2 && record2.version === 2) {
895
+ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
896
+ if ("input" in record3 && record3.version === 2) {
897
897
  return {
898
898
  version: 2,
899
- sourceId: record2.source.siteId,
900
- sourceRecordId: record2.source.recordId,
901
- type: record2.type,
902
- input: record2.input
899
+ sourceId: record3.source.siteId,
900
+ sourceRecordId: record3.source.recordId,
901
+ type: record3.type,
902
+ input: record3.input
903
903
  };
904
904
  }
905
- if ("input" in record2) {
905
+ if ("input" in record3) {
906
906
  return {
907
907
  version: 1,
908
908
  sourceId: fallbackSourceId,
909
- sourceRecordId: record2.hubRecordId,
910
- type: record2.type,
911
- input: record2.input
909
+ sourceRecordId: record3.hubRecordId,
910
+ type: record3.type,
911
+ input: record3.input
912
912
  };
913
913
  }
914
- if ("type" in record2 && record2.type === "company") {
915
- const input = { name: record2.name };
914
+ if ("type" in record3 && record3.type === "company") {
915
+ const input = { name: record3.name };
916
916
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
917
- if (record2[key]) input[key] = record2[key];
917
+ if (record3[key]) input[key] = record3[key];
918
918
  }
919
919
  return {
920
920
  version: 1,
921
921
  sourceId: fallbackSourceId,
922
- sourceRecordId: record2.hubRecordId,
922
+ sourceRecordId: record3.hubRecordId,
923
923
  type: "company",
924
924
  input
925
925
  };
@@ -927,28 +927,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
927
927
  return {
928
928
  version: 1,
929
929
  sourceId: fallbackSourceId,
930
- sourceRecordId: record2.hubRecordId,
930
+ sourceRecordId: record3.hubRecordId,
931
931
  type: "person",
932
- input: sharedPersonInput(record2)
932
+ input: sharedPersonInput(record3)
933
933
  };
934
934
  }
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`);
935
+ function sharedRecordFromCrm(crm, record3, target, sourceId) {
936
+ if (target.fields && !target.fields[record3.type]) {
937
+ throw new Error(`${target.name} does not accept "${record3.type}" records`);
938
938
  }
939
- const def = crm.type(record2.type);
940
- const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
939
+ const def = crm.type(record3.type);
940
+ const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
941
941
  if (!fields) {
942
- throw new Error(`${target.name} requires an explicit field allowlist for "${record2.type}" records`);
942
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
943
943
  }
944
944
  const nameField = def.nameField ?? "name";
945
945
  const input = {};
946
946
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
947
- const value = record2.fields?.[field];
947
+ const value = record3.fields?.[field];
948
948
  if (value !== void 0) input[field] = value;
949
949
  }
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 };
950
+ if (input[nameField] === void 0) input[nameField] = record3.name;
951
+ return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
952
952
  }
953
953
  async function upsertPerson(deps, opts) {
954
954
  const email = opts.email.toLowerCase();
@@ -962,11 +962,11 @@ async function upsertPerson(deps, opts) {
962
962
  const created = await createRecord(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
963
963
  return { recordId: created.id };
964
964
  }
965
- async function findSharedRecord(deps, record2, tag) {
965
+ async function findSharedRecord(deps, record3, tag) {
966
966
  const structured = await getRecordByOrigin(
967
967
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
968
- record2.sourceId,
969
- record2.sourceRecordId
968
+ record3.sourceId,
969
+ record3.sourceRecordId
970
970
  );
971
971
  if (structured) return structured;
972
972
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -975,53 +975,53 @@ async function findSharedRecord(deps, record2, tag) {
975
975
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
976
976
  if (found.crm_record?.[0]) return found.crm_record[0];
977
977
  }
978
- const def = deps.crm.type(record2.type);
978
+ const def = deps.crm.type(record3.type);
979
979
  const emailField = def.emailField;
980
- if (emailField && typeof record2.input[emailField] === "string") {
981
- const primaryEmail = record2.input[emailField].toLowerCase();
980
+ if (emailField && typeof record3.input[emailField] === "string") {
981
+ const primaryEmail = record3.input[emailField].toLowerCase();
982
982
  const found = await deps.db.query({
983
- crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
983
+ crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
984
984
  });
985
985
  if (found.crm_record?.[0]) return found.crm_record[0];
986
986
  }
987
- const domain = record2.input.domain;
987
+ const domain = record3.input.domain;
988
988
  const domainSlot = def.fields.domain?.slot;
989
989
  if (typeof domain === "string" && domainSlot) {
990
990
  const found = await deps.db.query({
991
- crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
991
+ crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
992
992
  });
993
993
  if (found.crm_record?.[0]) return found.crm_record[0];
994
994
  }
995
995
  const nameField = def.nameField ?? "name";
996
- const name = record2.input[nameField];
997
- if (record2.type === "company" && typeof name === "string" && name.trim()) {
996
+ const name = record3.input[nameField];
997
+ if (record3.type === "company" && typeof name === "string" && name.trim()) {
998
998
  const found = await deps.db.query({
999
- crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
999
+ crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
1000
1000
  });
1001
1001
  if (found.crm_record?.[0]) return found.crm_record[0];
1002
1002
  }
1003
1003
  return void 0;
1004
1004
  }
1005
1005
  async function projectSharedRecord(deps, shared, options = {}) {
1006
- const record2 = normalizeSharedRecord(shared, options.sourceId);
1007
- if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
1006
+ const record3 = normalizeSharedRecord(shared, options.sourceId);
1007
+ if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
1008
1008
  throw new Error("type, source site id, and source record id must be non-empty");
1009
1009
  }
1010
- if (options.sourceId && options.sourceId !== record2.sourceId) {
1010
+ if (options.sourceId && options.sourceId !== record3.sourceId) {
1011
1011
  throw new Error("signed sender does not match payload source");
1012
1012
  }
1013
- const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
1013
+ const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
1014
1014
  const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1015
- const existing = await findSharedRecord(deps, record2, tag);
1015
+ const existing = await findSharedRecord(deps, record3, tag);
1016
1016
  let recordId;
1017
1017
  if (existing && typeof existing.id === "string") {
1018
- await updateRecord(crmDeps3, { id: existing.id, input: record2.input });
1018
+ await updateRecord(crmDeps3, { id: existing.id, input: record3.input });
1019
1019
  recordId = existing.id;
1020
1020
  } else {
1021
- recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
1021
+ recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
1022
1022
  await createRecord({ ...crmDeps3, newId: () => recordId }, {
1023
- type: record2.type,
1024
- input: record2.input,
1023
+ type: record3.type,
1024
+ input: record3.input,
1025
1025
  mutationId: `share-create:${tag}`
1026
1026
  });
1027
1027
  }
@@ -1031,10 +1031,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
1031
1031
  );
1032
1032
  await upsertRecordOrigin(crmDeps3, {
1033
1033
  recordId,
1034
- sourceId: record2.sourceId,
1035
- sourceRecordId: record2.sourceRecordId,
1034
+ sourceId: record3.sourceId,
1035
+ sourceRecordId: record3.sourceRecordId,
1036
1036
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
1037
- payloadVersion: record2.version
1037
+ payloadVersion: record3.version
1038
1038
  });
1039
1039
  return { recordId };
1040
1040
  }
@@ -1483,7 +1483,8 @@ function applicationSummary(app) {
1483
1483
  meetingLink: app.meetingLink ?? null,
1484
1484
  paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
1485
1485
  renewalAt: app.renewalAt ?? null,
1486
- canceled: app.canceled === true
1486
+ canceled: app.canceled === true,
1487
+ tierId: app.tierId ?? null
1487
1488
  };
1488
1489
  }
1489
1490
  function memberApplication(app, meeting, defaultTimezone) {
@@ -1957,8 +1958,8 @@ var handleNetworkShared = async (req, url, env, ctx) => {
1957
1958
  payload,
1958
1959
  sourceId ? { sourceId } : {}
1959
1960
  );
1960
- const record2 = normalizeSharedRecord(payload, sourceId);
1961
- return json({ recordId, type: record2.type });
1961
+ const record3 = normalizeSharedRecord(payload, sourceId);
1962
+ return json({ recordId, type: record3.type });
1962
1963
  } catch (error) {
1963
1964
  return json({
1964
1965
  error: error instanceof Error ? error.message : "invalid shared record"
@@ -1998,9 +1999,9 @@ var MAX_BENEFITS = 24;
1998
1999
  var MAX_MARKDOWN = 65536;
1999
2000
  var MAX_LABEL = 160;
2000
2001
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2001
- function rejectUnknownKeys(value, allowed, path, errors) {
2002
+ function rejectUnknownKeys(value, allowed2, path, errors) {
2002
2003
  for (const key of Object.keys(value)) {
2003
- if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2004
+ if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2004
2005
  }
2005
2006
  }
2006
2007
  function boundedString(value, path, errors, max, required = false) {
@@ -2156,8 +2157,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2156
2157
  ]);
2157
2158
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2158
2159
  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`);
2160
+ function unknownKeys(value, allowed2, path, errors) {
2161
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2161
2162
  }
2162
2163
  function text2(value, path, errors, max = 65536) {
2163
2164
  if (typeof value !== "string" || !value.trim()) {
@@ -2242,10 +2243,10 @@ function policyOf(value, errors) {
2242
2243
  errors.push("document.policy must be an object");
2243
2244
  return void 0;
2244
2245
  }
2245
- const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2246
- unknownKeys(input, allowed, "document.policy", errors);
2246
+ const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2247
+ unknownKeys(input, allowed2, "document.policy", errors);
2247
2248
  const result = {};
2248
- for (const key of allowed) {
2249
+ for (const key of allowed2) {
2249
2250
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
2250
2251
  result[key] = text2(input[key], `document.policy.${key}`, errors);
2251
2252
  }
@@ -2384,9 +2385,589 @@ var handleSignupControl = async (req, url, env, ctx) => {
2384
2385
  });
2385
2386
  };
2386
2387
 
2387
- // src/worker-routes-admission-grants.ts
2388
+ // src/worker-routes-member-content.ts
2388
2389
  import { verifyFederatedRequest as verifyFederatedRequest3 } from "@odla-ai/db";
2390
+
2391
+ // src/member-content-canonical.ts
2392
+ var encoder2 = new TextEncoder();
2393
+ function canonicalValue2(value) {
2394
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2395
+ return JSON.stringify(value);
2396
+ }
2397
+ if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
2398
+ if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
2399
+ const record3 = value;
2400
+ const entries = Object.keys(record3).sort().flatMap((key) => {
2401
+ const item = record3[key];
2402
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
2403
+ });
2404
+ return `{${entries.join(",")}}`;
2405
+ }
2406
+ function canonicalMemberContent(value) {
2407
+ return canonicalValue2(value);
2408
+ }
2409
+ async function memberContentDigest(document) {
2410
+ const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
2411
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2412
+ }
2413
+
2414
+ // src/member-content-validation-helpers.ts
2415
+ var ID3 = /^[a-z][a-z0-9-]{0,79}$/;
2416
+ var SLUG2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2417
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2418
+ var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
2419
+ var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
2420
+ var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
2421
+ var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
2422
+ var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
2423
+ var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
2424
+ var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
2425
+ var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
2426
+ var MAX_PAGES = 100;
2427
+ var MAX_MARKDOWN2 = 65536;
2428
+ var MAX_TEXT = 240;
2429
+ var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2430
+ function unknownKeys2(value, allowed2, path, errors) {
2431
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2432
+ }
2433
+ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
2434
+ if (value === void 0) {
2435
+ if (required) errors.push(`${path} is required`);
2436
+ return void 0;
2437
+ }
2438
+ if (typeof value !== "string") {
2439
+ errors.push(`${path} must be a string`);
2440
+ return void 0;
2441
+ }
2442
+ const valueText = value.trim();
2443
+ if (required && !valueText) errors.push(`${path} is required`);
2444
+ if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2445
+ return valueText;
2446
+ }
2447
+ function offerValue(value, path, errors, tiers, audience) {
2448
+ const raw = record2(value);
2449
+ if (!raw) {
2450
+ errors.push(`${path} must be an object`);
2451
+ return void 0;
2452
+ }
2453
+ unknownKeys2(raw, OFFER_KEYS, path, errors);
2454
+ const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2455
+ if (tierId && !ID3.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2456
+ if (tiers.size && !tiers.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2457
+ if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2458
+ errors.push(`${path}.tierId must grant access to this page`);
2459
+ }
2460
+ const label = textValue(raw.label, `${path}.label`, errors);
2461
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2);
2462
+ const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
2463
+ return {
2464
+ tierId,
2465
+ ...label ? { label } : {},
2466
+ ...bodyMarkdown ? { bodyMarkdown } : {},
2467
+ ...ctaLabel ? { ctaLabel } : {}
2468
+ };
2469
+ }
2470
+
2471
+ // src/member-content-validation.ts
2472
+ function validateMemberContentDocument(input, options = {}) {
2473
+ const errors = [];
2474
+ const root = record2(input);
2475
+ if (!root) return { ok: false, errors: ["document must be an object"] };
2476
+ unknownKeys2(root, ROOT_KEYS3, "document", errors);
2477
+ if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
2478
+ if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
2479
+ if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
2480
+ const sourceRaw = record2(root.source);
2481
+ let source = { type: "chapter", appId: "invalid" };
2482
+ if (!sourceRaw) errors.push("document.source must be an object");
2483
+ else {
2484
+ unknownKeys2(sourceRaw, SOURCE_KEYS, "document.source", errors);
2485
+ const type = sourceRaw.type;
2486
+ const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
2487
+ if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
2488
+ if (appId && !ID3.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
2489
+ if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2490
+ if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
2491
+ if (type === "parent" || type === "chapter") source = { type, appId };
2492
+ }
2493
+ let target;
2494
+ if (root.target !== void 0) {
2495
+ const targetRaw = record2(root.target);
2496
+ if (!targetRaw) errors.push("document.target must be an object");
2497
+ else {
2498
+ unknownKeys2(targetRaw, TARGET_KEYS, "document.target", errors);
2499
+ const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
2500
+ const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
2501
+ target = { chapterId, environment };
2502
+ }
2503
+ }
2504
+ if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
2505
+ if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
2506
+ if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
2507
+ const rawPages = Array.isArray(root.pages) ? root.pages : [];
2508
+ if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
2509
+ const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
2510
+ const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
2511
+ const ids = /* @__PURE__ */ new Set();
2512
+ const slugs = /* @__PURE__ */ new Set();
2513
+ const pages = [];
2514
+ for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2515
+ const path = `document.pages[${index}]`;
2516
+ const raw = record2(value);
2517
+ if (!raw) {
2518
+ errors.push(`${path} must be an object`);
2519
+ continue;
2520
+ }
2521
+ unknownKeys2(raw, PAGE_KEYS, path, errors);
2522
+ const id = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
2523
+ const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
2524
+ const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2525
+ if (id && !ID3.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2526
+ if (slug && !SLUG2.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2527
+ if (ids.has(id) || knownParentIds.has(id)) errors.push(`${path}.id duplicates page "${id}"`);
2528
+ if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
2529
+ ids.add(id);
2530
+ slugs.add(slug);
2531
+ const navRaw = record2(raw.navigation);
2532
+ let navigation = { section: "General", order: index };
2533
+ if (!navRaw) errors.push(`${path}.navigation must be an object`);
2534
+ else {
2535
+ unknownKeys2(navRaw, NAV_KEYS, `${path}.navigation`, errors);
2536
+ const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
2537
+ const order = navRaw.order;
2538
+ if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
2539
+ errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
2540
+ }
2541
+ const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
2542
+ navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
2543
+ }
2544
+ const audienceRaw = record2(raw.audience);
2545
+ let audience = { kind: "all-members" };
2546
+ if (!audienceRaw) errors.push(`${path}.audience must be an object`);
2547
+ else {
2548
+ unknownKeys2(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
2549
+ if (audienceRaw.kind === "all-members") {
2550
+ if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
2551
+ } else if (audienceRaw.kind === "tiers") {
2552
+ if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
2553
+ errors.push(`${path}.audience.tierIds must be a non-empty array`);
2554
+ }
2555
+ const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
2556
+ const normalized = [];
2557
+ for (const [tierIndex, value2] of values.entries()) {
2558
+ const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
2559
+ if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
2560
+ if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
2561
+ normalized.push(tier);
2562
+ }
2563
+ audience = { kind: "tiers", tierIds: normalized };
2564
+ } else errors.push(`${path}.audience.kind must be all-members or tiers`);
2565
+ }
2566
+ const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
2567
+ const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN2);
2568
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, true) ?? "";
2569
+ let upsell;
2570
+ if (raw.upsell !== void 0) {
2571
+ const upsellRaw = record2(raw.upsell);
2572
+ if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
2573
+ else {
2574
+ unknownKeys2(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
2575
+ const owner = upsellRaw.owner;
2576
+ if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
2577
+ if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
2578
+ if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
2579
+ if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
2580
+ const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
2581
+ if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
2582
+ else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
2583
+ }
2584
+ }
2585
+ pages.push({ id, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
2586
+ }
2587
+ const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
2588
+ for (const [index, page] of pages.entries()) {
2589
+ const parentId = page.navigation.parentId;
2590
+ if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
2591
+ if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
2592
+ }
2593
+ for (const page of pages) {
2594
+ const seen = /* @__PURE__ */ new Set([page.id]);
2595
+ let parentId = page.navigation.parentId;
2596
+ while (parentId) {
2597
+ if (seen.has(parentId)) {
2598
+ errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
2599
+ break;
2600
+ }
2601
+ seen.add(parentId);
2602
+ parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
2603
+ }
2604
+ }
2605
+ let upsellOverrides;
2606
+ if (root.upsellOverrides !== void 0) {
2607
+ if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
2608
+ if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
2609
+ const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
2610
+ const seen = /* @__PURE__ */ new Set();
2611
+ upsellOverrides = [];
2612
+ for (const [index, value] of values.entries()) {
2613
+ const path = `document.upsellOverrides[${index}]`;
2614
+ const raw = record2(value);
2615
+ if (!raw) {
2616
+ errors.push(`${path} must be an object`);
2617
+ continue;
2618
+ }
2619
+ unknownKeys2(raw, OVERRIDE_KEYS, path, errors);
2620
+ const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
2621
+ if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
2622
+ seen.add(pageId);
2623
+ const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
2624
+ if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
2625
+ else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
2626
+ const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
2627
+ if (offer) upsellOverrides.push({ pageId, offer });
2628
+ }
2629
+ }
2630
+ if (errors.length) return { ok: false, errors };
2631
+ return { ok: true, value: {
2632
+ schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
2633
+ revision: Number(root.revision),
2634
+ generatedAt: Number(root.generatedAt),
2635
+ source,
2636
+ ...target ? { target } : {},
2637
+ pages,
2638
+ ...upsellOverrides ? { upsellOverrides } : {}
2639
+ } };
2640
+ }
2641
+
2642
+ // src/member-content.ts
2643
+ var MEMBER_CONTENT_SCHEMA_VERSION = 1;
2644
+ var MEMBER_CONTENT_PATH = "/api/member-content";
2645
+ var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
2646
+ 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);
2647
+ function composeMemberContent(parent, chapter) {
2648
+ const pages = [];
2649
+ const parentPages = /* @__PURE__ */ new Map();
2650
+ for (const page of parent?.pages ?? []) {
2651
+ const effective = { ...page, source: parent.source };
2652
+ parentPages.set(page.id, effective);
2653
+ pages.push(effective);
2654
+ }
2655
+ for (const override of chapter?.upsellOverrides ?? []) {
2656
+ const page = parentPages.get(override.pageId);
2657
+ if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
2658
+ }
2659
+ for (const page of chapter?.pages ?? []) {
2660
+ if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
2661
+ throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
2662
+ }
2663
+ pages.push({ ...page, source: chapter.source });
2664
+ }
2665
+ return {
2666
+ pages: pages.sort(pageOrder),
2667
+ parentRevision: parent?.revision ?? null,
2668
+ chapterRevision: chapter?.revision ?? null
2669
+ };
2670
+ }
2671
+ var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
2672
+ function projectMemberContent(content, viewer, tiers) {
2673
+ const authority = new Map(tiers.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
2674
+ return {
2675
+ viewer: { ...viewer },
2676
+ revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
2677
+ pages: content.pages.map((page) => {
2678
+ const canRead = allowed(page, viewer);
2679
+ const offer = !canRead ? page.upsell?.offer : void 0;
2680
+ const tier = offer ? authority.get(offer.tierId) : void 0;
2681
+ const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
2682
+ return {
2683
+ id: page.id,
2684
+ slug: page.slug,
2685
+ title: page.title,
2686
+ ...page.summary ? { summary: page.summary } : {},
2687
+ navigation: page.navigation,
2688
+ audience: page.audience,
2689
+ source: page.source,
2690
+ access: canRead ? "allowed" : "locked",
2691
+ ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
2692
+ ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
2693
+ ...upsell ? { upsell } : {}
2694
+ };
2695
+ })
2696
+ };
2697
+ }
2698
+
2699
+ // src/member-content-store.ts
2700
+ var control = (row) => row && row.document && typeof row.document === "object" ? row : null;
2701
+ var local = (row) => row && row.document && typeof row.document === "object" ? row : null;
2702
+ async function exists(db, ns, where) {
2703
+ if (db.aggregate) return Number((await db.aggregate(ns, { count: true }, { where })).count ?? 0) > 0;
2704
+ const rows4 = (await db.query({ [ns]: {} }))[ns] ?? [];
2705
+ return rows4.some((row) => Object.entries(where).every(([key, value]) => row[key] === value));
2706
+ }
2707
+ async function loadActiveMemberContentControl(db, targetId, environment) {
2708
+ const where = { targetId, environment, active: true };
2709
+ if (!await exists(db, "memberContentControls", where)) return null;
2710
+ const rows4 = (await db.query({ memberContentControls: { $: {
2711
+ where,
2712
+ order: { revision: "desc" },
2713
+ limit: 1
2714
+ } } })).memberContentControls;
2715
+ return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
2716
+ }
2717
+ async function loadLatestMemberContentControl(db, targetId, environment) {
2718
+ const where = { targetId, environment };
2719
+ if (!await exists(db, "memberContentControls", where)) return null;
2720
+ const rows4 = (await db.query({ memberContentControls: { $: {
2721
+ where,
2722
+ order: { revision: "desc" },
2723
+ limit: 1
2724
+ } } })).memberContentControls;
2725
+ return control(Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision) - Number(a.revision))[0] : void 0);
2726
+ }
2727
+ async function loadLocalMemberContent(db, chapterId, environment) {
2728
+ const id = `member-content-local:${environment}`;
2729
+ const rows4 = (await db.query({ memberContentLocal: { $: { where: { id }, limit: 1 } } })).memberContentLocal;
2730
+ return local(Array.isArray(rows4) ? rows4[0] : void 0);
2731
+ }
2732
+ async function materializeMemberContentControl(db, envelope, previous, receivedAt) {
2733
+ const { document, digest } = envelope;
2734
+ const target = document.target;
2735
+ const id = `member-content-control:${target.environment}:${document.revision}`;
2736
+ const ops = [];
2737
+ if (previous?.active) {
2738
+ ops.push({ t: "update", ns: "memberContentControls", id: previous.id, attrs: { active: false } });
2739
+ }
2740
+ ops.push({ t: "update", ns: "memberContentControls", id, attrs: {
2741
+ id,
2742
+ sourceId: document.source.appId,
2743
+ targetId: target.chapterId,
2744
+ environment: target.environment,
2745
+ revision: document.revision,
2746
+ digest,
2747
+ document,
2748
+ active: true,
2749
+ receivedAt
2750
+ } });
2751
+ await db.transact(ops, { mutationId: `member-content-control:${target.environment}:${document.revision}:${digest}` });
2752
+ }
2753
+ async function saveLocalMemberContent(db, document, environment, expectedRevision, updatedAt, updatedBy) {
2754
+ const id = `member-content-local:${environment}`;
2755
+ const current = await loadLocalMemberContent(db, document.source.appId, environment);
2756
+ if ((current?.revision ?? 0) !== expectedRevision) throw new Error("member_content_revision_conflict");
2757
+ const row = {
2758
+ id,
2759
+ chapterId: document.source.appId,
2760
+ environment,
2761
+ revision: document.revision,
2762
+ document,
2763
+ updatedAt,
2764
+ ...updatedBy ? { updatedBy } : {}
2765
+ };
2766
+ await db.transact([{ t: "update", ns: "memberContentLocal", id, attrs: row }], {
2767
+ mutationId: `member-content-local:${environment}:${document.revision}`,
2768
+ guards: current ? [{ ns: "memberContentLocal", id, exists: true, equals: { revision: expectedRevision } }] : [{ ns: "memberContentLocal", id, exists: false }]
2769
+ });
2770
+ return row;
2771
+ }
2772
+
2773
+ // src/worker-routes-member-content.ts
2774
+ var ADMIN_PATH = "/api/admin/member-content";
2775
+ var BODY_CAP = 1e6;
2389
2776
  var reject2 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2777
+ async function groupAndTiers(db, chapterId) {
2778
+ const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
2779
+ const group = Array.isArray(groups) ? groups[0] : void 0;
2780
+ if (!group) return { group: null, tiers: [] };
2781
+ return { group, tiers: await loadTiers(db, group) };
2782
+ }
2783
+ async function memberTierId(db, chapterId, email) {
2784
+ if (!email) return null;
2785
+ let rows4 = (await db.query({ applications: { $: {
2786
+ where: { emailNormalized: normalizeAdmissionEmail(email) },
2787
+ limit: 20
2788
+ } } })).applications;
2789
+ if (!rows4?.length) rows4 = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
2790
+ 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;
2791
+ return typeof latest?.tierId === "string" && latest.tierId ? latest.tierId : null;
2792
+ }
2793
+ async function activeDocuments(db, chapterId, environment) {
2794
+ const [parent, chapter] = await Promise.all([
2795
+ loadActiveMemberContentControl(db, chapterId, environment),
2796
+ loadLocalMemberContent(db, chapterId, environment)
2797
+ ]);
2798
+ return { parent, chapter };
2799
+ }
2800
+ var handleMemberContentControl = async (req, url, env, ctx) => {
2801
+ if (req.method !== "POST" || url.pathname !== MEMBER_CONTENT_CONTROL_PATH) return null;
2802
+ const settings = ctx.chapter.memberContent?.parent;
2803
+ if (!settings) return json({ error: "not found" }, 404);
2804
+ const db = ctx.makeDb(env);
2805
+ const secret = await getVaultSecret(db, settings.secretName);
2806
+ if (!secret) return json({ error: "unauthorized" }, 401);
2807
+ const verified = await verifyFederatedRequest3(req, { secret, senders: [settings.sourceId] });
2808
+ if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2809
+ let input;
2810
+ try {
2811
+ input = JSON.parse(verified.body);
2812
+ } catch {
2813
+ return reject2("invalid_document", 400, ["request body must be JSON"]);
2814
+ }
2815
+ const digest = typeof input.digest === "string" ? input.digest : "";
2816
+ const initial = validateMemberContentDocument(input.document, {
2817
+ sourceType: "parent"
2818
+ });
2819
+ if (!initial.ok) return reject2("invalid_document", 400, initial.errors);
2820
+ const document = initial.value;
2821
+ if (document.source.appId !== settings.sourceId) return reject2("wrong_source", 403);
2822
+ if (document.target?.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
2823
+ if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
2824
+ const { tiers } = await groupAndTiers(db, ctx.chapter.id);
2825
+ const checked = validateMemberContentDocument(document, {
2826
+ sourceType: "parent",
2827
+ sourceAppId: settings.sourceId,
2828
+ tiers
2829
+ });
2830
+ if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
2831
+ const local2 = await loadLocalMemberContent(db, ctx.chapter.id, env.ODLA_ENV);
2832
+ if (local2) {
2833
+ const localCheck = validateMemberContentDocument(local2.document, {
2834
+ sourceType: "chapter",
2835
+ sourceAppId: ctx.chapter.id,
2836
+ tiers,
2837
+ parentPages: document.pages
2838
+ });
2839
+ if (!localCheck.ok) {
2840
+ return reject2("invalid_document", 409, localCheck.errors.map((error) => `existing chapter content: ${error}`));
2841
+ }
2842
+ }
2843
+ if (!digest || await memberContentDigest(document) !== digest) return reject2("invalid_digest", 400);
2844
+ const latest = await loadLatestMemberContentControl(db, ctx.chapter.id, env.ODLA_ENV);
2845
+ if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
2846
+ if (latest && document.revision === latest.revision) {
2847
+ if (latest.digest !== digest) return reject2("conflicting_revision", 409);
2848
+ return json({
2849
+ ok: true,
2850
+ target: document.target,
2851
+ revision: latest.revision,
2852
+ digest: latest.digest,
2853
+ status: "replayed",
2854
+ acknowledgedAt: latest.receivedAt
2855
+ });
2856
+ }
2857
+ const receivedAt = Date.now();
2858
+ try {
2859
+ await materializeMemberContentControl(db, { digest, document }, latest, receivedAt);
2860
+ } catch {
2861
+ return reject2("materialization_failed", 503);
2862
+ }
2863
+ return json({
2864
+ ok: true,
2865
+ target: document.target,
2866
+ revision: document.revision,
2867
+ digest,
2868
+ status: "accepted",
2869
+ acknowledgedAt: receivedAt
2870
+ });
2871
+ };
2872
+ var handleMemberContent = async (req, url, env, ctx) => {
2873
+ if (req.method !== "GET" || url.pathname !== MEMBER_CONTENT_PATH) return null;
2874
+ if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
2875
+ const user = await ctx.verifyUser(req, env);
2876
+ if (!user) return json({ error: "unauthorized" }, 401);
2877
+ const db = ctx.makeDb(env);
2878
+ const role = await ctx.roleFor(db, user);
2879
+ const admin = await ctx.isAdmin(db, user);
2880
+ const roleIndex = ctx.chapter.auth.ladder.indexOf(role);
2881
+ const memberIndex = Math.max(0, ctx.chapter.auth.ladder.length - 2);
2882
+ if (!admin && roleIndex < memberIndex) return json({ error: "member access required" }, 403);
2883
+ const [{ parent, chapter }, { group, tiers }, tierId] = await Promise.all([
2884
+ activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
2885
+ groupAndTiers(db, ctx.chapter.id),
2886
+ memberTierId(db, ctx.chapter.id, user.email)
2887
+ ]);
2888
+ const effective = composeMemberContent(parent?.document, chapter?.document);
2889
+ let offerable = tiers;
2890
+ if (group) {
2891
+ const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
2892
+ offerable = await loadOfferableTiers(db, group, Boolean(stripeSecret));
2893
+ }
2894
+ return json(projectMemberContent(effective, { tierId, admin }, offerable));
2895
+ };
2896
+ var handleAdminMemberContent = async (req, url, env, ctx) => {
2897
+ if (url.pathname !== ADMIN_PATH) return null;
2898
+ if (!ctx.chapter.memberContent) return json({ error: "not found" }, 404);
2899
+ const user = await ctx.verifyUser(req, env);
2900
+ if (!user) return json({ error: "unauthorized" }, 401);
2901
+ const db = ctx.makeDb(env);
2902
+ if (!await ctx.isAdmin(db, user)) return json({ error: "forbidden" }, 403);
2903
+ const [{ parent, chapter }, { tiers }] = await Promise.all([
2904
+ activeDocuments(db, ctx.chapter.id, env.ODLA_ENV),
2905
+ groupAndTiers(db, ctx.chapter.id)
2906
+ ]);
2907
+ if (req.method === "GET") {
2908
+ const effective = composeMemberContent(parent?.document, chapter?.document);
2909
+ return json({
2910
+ parent: parent ? { revision: parent.revision, digest: parent.digest, document: parent.document } : null,
2911
+ chapter: chapter ? { revision: chapter.revision, document: chapter.document } : null,
2912
+ effective,
2913
+ tiers: tiers.map(({ id, name, priceCents, active }) => ({ id, name, priceCents, active })),
2914
+ localAuthoring: ctx.chapter.memberContent.localAuthoring
2915
+ });
2916
+ }
2917
+ if (req.method !== "PUT") return json({ error: "method not allowed" }, 405);
2918
+ if (!ctx.chapter.memberContent.localAuthoring) return json({ error: "local authoring is disabled" }, 403);
2919
+ const raw = await req.text();
2920
+ if (raw.length > BODY_CAP) return json({ error: "request body too large" }, 413);
2921
+ let input;
2922
+ try {
2923
+ input = JSON.parse(raw);
2924
+ } catch {
2925
+ return json({ error: "invalid JSON body" }, 400);
2926
+ }
2927
+ const expectedRevision = Number(input.expectedRevision);
2928
+ if (!Number.isInteger(expectedRevision) || expectedRevision < 0) {
2929
+ return json({ error: "expectedRevision must be a non-negative integer" }, 400);
2930
+ }
2931
+ const now = Date.now();
2932
+ const documentInput = withLocalAuthority(input.document, expectedRevision + 1, now, ctx.chapter.id);
2933
+ const checked = validateMemberContentDocument(documentInput, {
2934
+ sourceType: "chapter",
2935
+ sourceAppId: ctx.chapter.id,
2936
+ tiers,
2937
+ parentPages: parent?.document.pages ?? []
2938
+ });
2939
+ if (!checked.ok) return json({ error: "invalid member content", errors: checked.errors }, 400);
2940
+ try {
2941
+ const saved = await saveLocalMemberContent(
2942
+ db,
2943
+ checked.value,
2944
+ env.ODLA_ENV,
2945
+ expectedRevision,
2946
+ now,
2947
+ user.email ?? user.userId
2948
+ );
2949
+ return json({ ok: true, revision: saved.revision, document: saved.document });
2950
+ } catch (error) {
2951
+ if (error instanceof Error && error.message === "member_content_revision_conflict") {
2952
+ return json({ error: "member content changed; reload before saving" }, 409);
2953
+ }
2954
+ return json({ error: "member content could not be saved" }, 503);
2955
+ }
2956
+ };
2957
+ function withLocalAuthority(value, revision, generatedAt, appId) {
2958
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
2959
+ return {
2960
+ ...value,
2961
+ schemaVersion: 1,
2962
+ revision,
2963
+ generatedAt,
2964
+ source: { type: "chapter", appId }
2965
+ };
2966
+ }
2967
+
2968
+ // src/worker-routes-admission-grants.ts
2969
+ import { verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
2970
+ var reject3 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2390
2971
  var handleAdmissionGrants = async (req, url, env, ctx) => {
2391
2972
  if (req.method !== "POST" || url.pathname !== ADMISSION_GRANTS_PATH) return null;
2392
2973
  const authority = ctx.chapter.signupControl;
@@ -2394,26 +2975,26 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
2394
2975
  const db = ctx.makeDb(env);
2395
2976
  const secret = await getVaultSecret(db, authority.secretName);
2396
2977
  if (!secret) return json({ error: "unauthorized" }, 401);
2397
- const verified = await verifyFederatedRequest3(req, { secret, senders: [authority.sourceId] });
2978
+ const verified = await verifyFederatedRequest4(req, { secret, senders: [authority.sourceId] });
2398
2979
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2399
2980
  let input;
2400
2981
  try {
2401
2982
  input = JSON.parse(verified.body);
2402
2983
  } catch {
2403
- return reject2("invalid_document", 400, ["request body must be JSON"]);
2984
+ return reject3("invalid_document", 400, ["request body must be JSON"]);
2404
2985
  }
2405
2986
  const digest = typeof input.digest === "string" ? input.digest : "";
2406
2987
  const checked = validateAdmissionGrantDocument(input.document);
2407
- if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
2988
+ if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
2408
2989
  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);
2990
+ if (document.source.appId !== authority.sourceId) return reject3("wrong_source", 403);
2991
+ if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
2992
+ if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
2993
+ if (!digest || await signupControlDigest(document) !== digest) return reject3("invalid_digest", 400);
2413
2994
  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);
2995
+ if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
2415
2996
  if (latest && document.revision === latest.revision) {
2416
- if (latest.digest !== digest) return reject2("conflicting_revision", 409);
2997
+ if (latest.digest !== digest) return reject3("conflicting_revision", 409);
2417
2998
  return json({
2418
2999
  ok: true,
2419
3000
  target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
@@ -2427,7 +3008,7 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
2427
3008
  try {
2428
3009
  await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
2429
3010
  } catch {
2430
- return reject2("materialization_failed", 503);
3011
+ return reject3("materialization_failed", 503);
2431
3012
  }
2432
3013
  return json({
2433
3014
  ok: true,
@@ -3255,8 +3836,8 @@ async function startSubscription(req, env, ctx) {
3255
3836
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
3256
3837
  if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
3257
3838
  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());
3839
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3840
+ const offer = findGiftOffer(control2?.document.giftOffers, app.giftOfferId, Date.now());
3260
3841
  if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
3261
3842
  return json({ error: "gift offer is unavailable" }, 409);
3262
3843
  }
@@ -3415,14 +3996,14 @@ var handleGifts = async (req, url, env, ctx) => {
3415
3996
  if (req.method === "GET" && url.pathname === "/api/gifts") {
3416
3997
  const user = await userFor(req, env, ctx);
3417
3998
  if (!user) return json({ error: "unauthorized" }, 401);
3418
- const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3999
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3419
4000
  const found = await db.query({ giftClaims: { $: { where: {
3420
4001
  purchaserEmailNormalized: normalizeAdmissionEmail(user.email),
3421
4002
  chapterId: ctx.chapter.id
3422
4003
  }, limit: 200 } } });
3423
4004
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3424
4005
  return json({
3425
- offers: publicGiftOffers(control?.document.giftOffers, Date.now()),
4006
+ offers: publicGiftOffers(control2?.document.giftOffers, Date.now()),
3426
4007
  claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
3427
4008
  refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
3428
4009
  });
@@ -3438,8 +4019,8 @@ var handleGifts = async (req, url, env, ctx) => {
3438
4019
  }
3439
4020
  const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
3440
4021
  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());
4022
+ const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4023
+ const offer = findGiftOffer(control2?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
3443
4024
  if (!offer) return json({ error: "gift offer is unavailable" }, 409);
3444
4025
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3445
4026
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
@@ -4360,7 +4941,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
4360
4941
 
4361
4942
  // src/worker-routes-network.ts
4362
4943
  import { getRecord, typeSummary } from "@odla-ai/crm";
4363
- import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
4944
+ import { signFederatedRequest as signFederatedRequest2, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
4364
4945
 
4365
4946
  // src/worker-network-fetch.ts
4366
4947
  function targetFetcher(env, target) {
@@ -4379,7 +4960,7 @@ function fetchNetworkTarget(env, target, destination, init) {
4379
4960
  // src/worker-network-push.ts
4380
4961
  import { recordDeliveryAttempt } from "@odla-ai/crm";
4381
4962
  import { signFederatedRequest } from "@odla-ai/db";
4382
- async function pushNetworkRecord(db, env, ctx, target, record2) {
4963
+ async function pushNetworkRecord(db, env, ctx, target, record3) {
4383
4964
  const secret = await getVaultSecret(db, target.secretName);
4384
4965
  const crmDeps3 = {
4385
4966
  crm: ctx.chapter.crm,
@@ -4390,7 +4971,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4390
4971
  if (!secret) {
4391
4972
  const error = `vault secret "${target.secretName}" is missing`;
4392
4973
  await recordDeliveryAttempt(crmDeps3, {
4393
- recordId: record2.id,
4974
+ recordId: record3.id,
4394
4975
  targetId: target.id,
4395
4976
  status: "failed",
4396
4977
  payloadVersion: 2,
@@ -4400,7 +4981,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4400
4981
  }
4401
4982
  let payload;
4402
4983
  try {
4403
- payload = sharedRecordFromCrm(ctx.chapter.crm, record2, target, ctx.chapter.id);
4984
+ payload = sharedRecordFromCrm(ctx.chapter.crm, record3, target, ctx.chapter.id);
4404
4985
  } catch (err) {
4405
4986
  return {
4406
4987
  id: target.id,
@@ -4429,7 +5010,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4429
5010
  if (!res.ok) {
4430
5011
  const error = responseBody.error ?? "follower rejected the record";
4431
5012
  await recordDeliveryAttempt(crmDeps3, {
4432
- recordId: record2.id,
5013
+ recordId: record3.id,
4433
5014
  targetId: target.id,
4434
5015
  status: "failed",
4435
5016
  payloadVersion: 2,
@@ -4444,7 +5025,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4444
5025
  };
4445
5026
  }
4446
5027
  await recordDeliveryAttempt(crmDeps3, {
4447
- recordId: record2.id,
5028
+ recordId: record3.id,
4448
5029
  targetId: target.id,
4449
5030
  status: "delivered",
4450
5031
  payloadVersion: 2,
@@ -4460,7 +5041,7 @@ async function pushNetworkRecord(db, env, ctx, target, record2) {
4460
5041
  } catch (err) {
4461
5042
  const error = err instanceof Error ? err.message : "delivery failed";
4462
5043
  await recordDeliveryAttempt(crmDeps3, {
4463
- recordId: record2.id,
5044
+ recordId: record3.id,
4464
5045
  targetId: target.id,
4465
5046
  status: "failed",
4466
5047
  payloadVersion: 2,
@@ -4497,7 +5078,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
4497
5078
  const db = ctx.makeDb(env);
4498
5079
  const secret = await getVaultSecret(db, "network_share_secret");
4499
5080
  if (!secret) return json({ error: "unauthorized" }, 401);
4500
- const verified = await verifyFederatedRequest4(req, { secret });
5081
+ const verified = await verifyFederatedRequest5(req, { secret });
4501
5082
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
4502
5083
  const types = await Promise.all(
4503
5084
  Object.keys(ctx.chapter.crm.config.types).map((type) => typeSummary({ crm: ctx.chapter.crm, db }, type))
@@ -4598,17 +5179,17 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
4598
5179
  if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
4599
5180
  const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
4600
5181
  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);
5182
+ const record3 = await getRecord({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
5183
+ if (!record3) return json({ error: "record not found" }, 404);
4603
5184
  const results = await Promise.all(
4604
- targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record2))
5185
+ targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record3))
4605
5186
  );
4606
5187
  return json({ ok: results.every((result) => result.ok), results });
4607
5188
  };
4608
5189
 
4609
5190
  // src/worker-routes-network-records.ts
4610
5191
  import { listRecords } from "@odla-ai/crm";
4611
- import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
5192
+ import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest6 } from "@odla-ai/db";
4612
5193
  var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
4613
5194
  var MAX_PAGE = 50;
4614
5195
  var MAX_OFFSET = 1e4;
@@ -4626,18 +5207,18 @@ function integer(value, fallback, min, max) {
4626
5207
  const parsed = Number(value);
4627
5208
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
4628
5209
  }
4629
- function projectRecord(record2, allowed) {
5210
+ function projectRecord(record3, allowed2) {
4630
5211
  const fields = Object.fromEntries(
4631
- allowed.flatMap((name) => Object.prototype.hasOwnProperty.call(record2.fields, name) ? [[name, record2.fields[name]]] : [])
5212
+ allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record3.fields, name) ? [[name, record3.fields[name]]] : [])
4632
5213
  );
4633
5214
  return {
4634
- id: record2.id,
4635
- type: record2.type,
4636
- name: record2.name,
4637
- ...record2.stage ? { stage: record2.stage } : {},
5215
+ id: record3.id,
5216
+ type: record3.type,
5217
+ name: record3.name,
5218
+ ...record3.stage ? { stage: record3.stage } : {},
4638
5219
  fields,
4639
- createdAt: record2.createdAt,
4640
- updatedAt: record2.updatedAt
5220
+ createdAt: record3.createdAt,
5221
+ updatedAt: record3.updatedAt
4641
5222
  };
4642
5223
  }
4643
5224
  var handleNetworkRecords = async (req, url, env, ctx) => {
@@ -4646,15 +5227,15 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
4646
5227
  const secret = await getVaultSecret(db, "network_share_secret");
4647
5228
  const readers = ctx.chapter.network.readers;
4648
5229
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
4649
- const verified = await verifyFederatedRequest5(req, {
5230
+ const verified = await verifyFederatedRequest6(req, {
4650
5231
  secret,
4651
5232
  senders: readers.map((reader2) => reader2.id)
4652
5233
  });
4653
5234
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
4654
5235
  const reader = readers.find((candidate) => candidate.id === verified.sender);
4655
5236
  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);
5237
+ const allowed2 = reader?.fields[type];
5238
+ if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
4658
5239
  const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
4659
5240
  const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
4660
5241
  const search = (url.searchParams.get("search") ?? "").trim();
@@ -4675,7 +5256,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
4675
5256
  version: 1,
4676
5257
  site: { id: ctx.chapter.id, name: ctx.chapter.name },
4677
5258
  type,
4678
- records: page.records.map((record2) => projectRecord(record2, allowed)),
5259
+ records: page.records.map((record3) => projectRecord(record3, allowed2)),
4679
5260
  total: page.total,
4680
5261
  limit: page.limit,
4681
5262
  offset: page.offset
@@ -4830,7 +5411,7 @@ import {
4830
5411
  CRM_NS,
4831
5412
  getRecord as getRecord2
4832
5413
  } from "@odla-ai/crm";
4833
- import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest6 } from "@odla-ai/db";
5414
+ import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest7 } from "@odla-ai/db";
4834
5415
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
4835
5416
  var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
4836
5417
  var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
@@ -4889,7 +5470,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
4889
5470
  const secret = await getVaultSecret(db, "network_share_secret");
4890
5471
  const readers = ctx.chapter.network.readers;
4891
5472
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
4892
- const verified = await verifyFederatedRequest6(req, {
5473
+ const verified = await verifyFederatedRequest7(req, {
4893
5474
  secret,
4894
5475
  senders: readers.map((reader2) => reader2.id)
4895
5476
  });
@@ -4900,8 +5481,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
4900
5481
  if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
4901
5482
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
4902
5483
  }
4903
- const record2 = await getRecord2({ crm: ctx.chapter.crm, db }, recordId);
4904
- if (!record2 || record2.type !== recordType) return json({ error: "record not found" }, 404);
5484
+ const record3 = await getRecord2({ crm: ctx.chapter.crm, db }, recordId);
5485
+ if (!record3 || record3.type !== recordType) return json({ error: "record not found" }, 404);
4905
5486
  if (req.method === "GET") {
4906
5487
  return json({
4907
5488
  version: 1,
@@ -5055,15 +5636,15 @@ var handleFormation = async (req, url, env, ctx) => {
5055
5636
  if (submissionId && !SUBMISSION.test(submissionId)) {
5056
5637
  return json({ error: "submissionId must be an 8\u2013128 character base64url value" }, 400);
5057
5638
  }
5058
- const allowed = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
5059
- const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed.has(field));
5639
+ const allowed2 = /* @__PURE__ */ new Set([...formation.required, ...formation.optional]);
5640
+ const unknown = Object.keys(body).filter((field) => field !== "submissionId" && !allowed2.has(field));
5060
5641
  if (unknown.length) return json({ error: `field "${unknown[0]}" is not accepted` }, 400);
5061
5642
  for (const field of formation.required) {
5062
5643
  const value = body[field];
5063
5644
  if (typeof value !== "string" || value.trim() === "") return json({ error: `${field} is required` }, 400);
5064
5645
  }
5065
5646
  const input = {};
5066
- for (const field of allowed) {
5647
+ for (const field of allowed2) {
5067
5648
  const value = body[field];
5068
5649
  if (value === void 0) continue;
5069
5650
  const cap = formation.maxLen[field] ?? formation.defaultMaxLen;
@@ -5131,12 +5712,37 @@ async function deliverSignupControl(options) {
5131
5712
  return result;
5132
5713
  }
5133
5714
 
5134
- // src/admission-grants-delivery.ts
5715
+ // src/member-content-delivery.ts
5135
5716
  import { signFederatedRequest as signFederatedRequest6 } from "@odla-ai/db";
5717
+ async function deliverMemberContent(options) {
5718
+ const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
5719
+ const body = JSON.stringify(options.envelope);
5720
+ const signed = await signFederatedRequest6({
5721
+ secret: options.secret,
5722
+ sender: options.sender,
5723
+ method: "POST",
5724
+ url: destination,
5725
+ body
5726
+ });
5727
+ const response = await (options.fetch ?? globalThis.fetch)(destination, {
5728
+ method: "POST",
5729
+ headers: { ...signed, "content-type": "application/json" },
5730
+ body
5731
+ });
5732
+ const result = await response.json();
5733
+ if (!response.ok || !("ok" in result) || result.ok !== true) {
5734
+ const failure = result;
5735
+ throw new Error(`member content rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
5736
+ }
5737
+ return result;
5738
+ }
5739
+
5740
+ // src/admission-grants-delivery.ts
5741
+ import { signFederatedRequest as signFederatedRequest7 } from "@odla-ai/db";
5136
5742
  async function deliverAdmissionGrants(options) {
5137
5743
  const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
5138
5744
  const body = JSON.stringify(options.envelope);
5139
- const signed = await signFederatedRequest6({
5745
+ const signed = await signFederatedRequest7({
5140
5746
  secret: options.secret,
5141
5747
  sender: options.sender,
5142
5748
  method: "POST",
@@ -5295,12 +5901,14 @@ var BUILTIN_ROUTES = [
5295
5901
  handleCrm,
5296
5902
  handleNetworkShared,
5297
5903
  handleSignupControl,
5904
+ handleMemberContentControl,
5298
5905
  handleAdmissionGrants,
5299
5906
  handleNetworkSnapshot,
5300
5907
  handleNetworkRecords,
5301
5908
  handleNetworkSharedNotes,
5302
5909
  handleFormation,
5303
5910
  handleMember,
5911
+ handleMemberContent,
5304
5912
  handleGifts,
5305
5913
  handleSchedule,
5306
5914
  handlePayments,
@@ -5316,6 +5924,7 @@ var BUILTIN_ROUTES = [
5316
5924
  // Aggregation
5317
5925
  handleAdminDashboard,
5318
5926
  handleAdminBilling,
5927
+ handleAdminMemberContent,
5319
5928
  // Lifecycle actions
5320
5929
  handleAdminMeetingReschedule,
5321
5930
  handleAdminMeetingCancel,
@@ -5357,6 +5966,7 @@ export {
5357
5966
  chapterWorker,
5358
5967
  createWorkerContext,
5359
5968
  deliverAdmissionGrants,
5969
+ deliverMemberContent,
5360
5970
  deliverSignupControl,
5361
5971
  renderActiveSignupControlHtml,
5362
5972
  withActiveSignupControlTheme