@odla-ai/chapter 0.45.0 → 0.47.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.
@@ -80,12 +80,16 @@ function createWorkerContext(options) {
80
80
  }
81
81
  async function isAdminEmail(db, email) {
82
82
  if (!email) return false;
83
- const { admins } = await db.query({ admins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });
83
+ const { admins } = await db.query({ admins: { $: { where: { email: email.trim().toLowerCase() }, limit: 1 } } });
84
84
  return Array.isArray(admins) && admins.length > 0;
85
85
  }
86
86
  async function isSuperAdminEmail(db, email) {
87
87
  if (!auth.superAdmins || !email) return false;
88
- const { superAdmins } = await db.query({ superAdmins: { $: { where: { email: email.toLowerCase() }, limit: 1 } } });
88
+ if (auth.superAdminSource === "odla") {
89
+ const { $users } = await db.query({ $users: { $: { where: { email: email.trim().toLowerCase(), role: "super_admin" }, limit: 1 } } });
90
+ return Array.isArray($users) && $users.length > 0;
91
+ }
92
+ const { superAdmins } = await db.query({ superAdmins: { $: { where: { email: email.trim().toLowerCase() }, limit: 1 } } });
89
93
  return Array.isArray(superAdmins) && superAdmins.length > 0;
90
94
  }
91
95
  async function roleFor(db, u) {
@@ -724,8 +728,8 @@ var APPLICATION_LABELS = {
724
728
  state: "State"
725
729
  };
726
730
  function humanize(id) {
727
- const text5 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
728
- return text5 ? `${text5[0].toUpperCase()}${text5.slice(1)}` : id;
731
+ const text6 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
732
+ return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id;
729
733
  }
730
734
  function applicationDefs(chapter) {
731
735
  try {
@@ -1260,10 +1264,10 @@ function planDelivery(input) {
1260
1264
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1261
1265
  const to = redirect ? input.group.debugEmail : input.to;
1262
1266
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1263
- const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
1267
+ const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
1264
1268
 
1265
1269
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1266
- return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
1270
+ return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
1267
1271
  }
1268
1272
 
1269
1273
  // src/notify.ts
@@ -2151,10 +2155,10 @@ function boundedString(value, path, errors, max, required = false) {
2151
2155
  errors.push(`${path} must be a string`);
2152
2156
  return void 0;
2153
2157
  }
2154
- const text5 = value.trim();
2155
- if (required && !text5) errors.push(`${path} is required`);
2156
- if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2157
- return text5;
2158
+ const text6 = value.trim();
2159
+ if (required && !text6) errors.push(`${path} is required`);
2160
+ if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2161
+ return text6;
2158
2162
  }
2159
2163
  function validateMembershipPageDocument(input, options = {}) {
2160
2164
  const errors = [];
@@ -2203,8 +2207,8 @@ function validateMembershipPageDocument(input, options = {}) {
2203
2207
  else {
2204
2208
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
2205
2209
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
2206
- const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2207
- return text5 ? [text5] : [];
2210
+ const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2211
+ return text6 ? [text6] : [];
2208
2212
  });
2209
2213
  }
2210
2214
  }
@@ -3916,17 +3920,17 @@ async function notifyAdminOfPayment(db, env, eventId, app) {
3916
3920
  try {
3917
3921
  const group = await groupFor(db, app);
3918
3922
  if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
3919
- const text5 = (value) => typeof value === "string" ? value : "";
3923
+ const text6 = (value) => typeof value === "string" ? value : "";
3920
3924
  await sendTemplated(deps(db, env), {
3921
3925
  group: emailGroupFrom(group),
3922
3926
  template: "adminNotification",
3923
3927
  to: group.notificationEmail,
3924
3928
  vars: {
3925
- firstName: text5(app.firstName),
3926
- lastName: text5(app.lastName),
3927
- email: text5(app.email),
3928
- phone: text5(app.phone),
3929
- state: text5(app.state)
3929
+ firstName: text6(app.firstName),
3930
+ lastName: text6(app.lastName),
3931
+ email: text6(app.email),
3932
+ phone: text6(app.phone),
3933
+ state: text6(app.state)
3930
3934
  },
3931
3935
  dedupeKey: `${eventId}:admin`,
3932
3936
  applicationId: String(app.id)
@@ -5690,11 +5694,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
5690
5694
  for (const key of EMAIL_TEMPLATE_NAMES) {
5691
5695
  const t = templates[key];
5692
5696
  const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
5693
- const text5 = typeof t?.text === "string" ? t.text : "";
5694
- if (!subject || !text5.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
5697
+ const text6 = typeof t?.text === "string" ? t.text : "";
5698
+ if (!subject || !text6.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
5695
5699
  if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
5696
- if (text5.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
5697
- clean[key] = { subject, text: text5, enabled: t?.enabled !== false };
5700
+ if (text6.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
5701
+ clean[key] = { subject, text: text6, enabled: t?.enabled !== false };
5698
5702
  }
5699
5703
  const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5700
5704
  const notificationEmail = str2(body.notificationEmail).trim();
@@ -5911,7 +5915,8 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
5911
5915
  fields,
5912
5916
  sharedNotes,
5913
5917
  editableFields,
5914
- stageTransitions
5918
+ stageTransitions,
5919
+ commercialParity
5915
5920
  }) => ({
5916
5921
  id,
5917
5922
  name,
@@ -5919,7 +5924,8 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
5919
5924
  types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
5920
5925
  sharedNotes,
5921
5926
  editableFields,
5922
- stageTransitions
5927
+ stageTransitions,
5928
+ commercialParity
5923
5929
  }))
5924
5930
  });
5925
5931
  };
@@ -7181,9 +7187,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
7181
7187
  if (req.method === "POST") {
7182
7188
  let body;
7183
7189
  try {
7184
- const text5 = await req.text();
7185
- if (new TextEncoder().encode(text5).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
7186
- const parsed = JSON.parse(text5);
7190
+ const text6 = await req.text();
7191
+ if (new TextEncoder().encode(text6).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
7192
+ const parsed = JSON.parse(text6);
7187
7193
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
7188
7194
  body = parsed;
7189
7195
  } catch {
@@ -7778,6 +7784,298 @@ function reconcileStripeSnapshot(shadow, snapshots, sources) {
7778
7784
  };
7779
7785
  }
7780
7786
 
7787
+ // src/worker-routes-network-commercial-parity.ts
7788
+ import { signFederatedRequest as signFederatedRequest6, verifyFederatedRequest as verifyFederatedRequest8 } from "@odla-ai/db";
7789
+
7790
+ // src/network-commercial-parity.ts
7791
+ var DIGEST = /^[0-9a-f]{64}$/;
7792
+ var MAX_RECORDS = 500;
7793
+ var text4 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
7794
+ function parityMessage(partition, kind, value) {
7795
+ return [
7796
+ "chapter-commercial-parity",
7797
+ "v1",
7798
+ partition.appId,
7799
+ partition.chapterId,
7800
+ partition.environment,
7801
+ partition.dataEnvironment,
7802
+ partition.runtime,
7803
+ kind,
7804
+ value
7805
+ ].join("");
7806
+ }
7807
+ async function parityKey(secret) {
7808
+ const encoder4 = new TextEncoder();
7809
+ return crypto.subtle.importKey(
7810
+ "raw",
7811
+ encoder4.encode(secret),
7812
+ { name: "HMAC", hash: "SHA-256" },
7813
+ false,
7814
+ ["sign"]
7815
+ );
7816
+ }
7817
+ async function digestWithKey(key, partition, kind, value) {
7818
+ const encoder4 = new TextEncoder();
7819
+ const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
7820
+ return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
7821
+ }
7822
+ async function buildChapterCommercialParitySnapshot(input) {
7823
+ const requestedMax = input.maxRecords ?? MAX_RECORDS;
7824
+ const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
7825
+ const eligible = input.rows.filter((row) => text4(row.id) && (text4(row.stripeCustomerId) || text4(row.stripeSubscriptionId)));
7826
+ const selected2 = eligible.slice(0, maxRecords);
7827
+ const key = await parityKey(input.secret);
7828
+ const records = await Promise.all(selected2.map(async (row) => {
7829
+ const applicationId = text4(row.id);
7830
+ const customerId = text4(row.stripeCustomerId);
7831
+ const subscriptionId = text4(row.stripeSubscriptionId);
7832
+ return {
7833
+ applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
7834
+ customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
7835
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
7836
+ status: text4(row.status, 64),
7837
+ canceled: row.canceled === true
7838
+ };
7839
+ }));
7840
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
7841
+ const truncated = input.scanTruncated || eligible.length > maxRecords;
7842
+ return {
7843
+ version: 1,
7844
+ site: { id: input.siteId },
7845
+ partition: input.partition,
7846
+ generatedAt: input.now ?? Date.now(),
7847
+ complete: !truncated,
7848
+ truncated,
7849
+ counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
7850
+ records
7851
+ };
7852
+ }
7853
+ function recordKey(record4) {
7854
+ return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
7855
+ }
7856
+ function validSnapshot(snapshot, source) {
7857
+ const partition = snapshot?.partition;
7858
+ const records = snapshot?.records;
7859
+ if (snapshot?.version !== 1 || snapshot.site?.id !== source.chapterId || !partition || !Array.isArray(records) || records.length > MAX_RECORDS || partition.appId !== source.appId || partition.chapterId !== source.chapterId || partition.environment !== source.environment || partition.dataEnvironment !== source.dataEnvironment || partition.runtime !== source.runtime || typeof snapshot.complete !== "boolean" || typeof snapshot.truncated !== "boolean" || !snapshot.counts) return false;
7860
+ const seen = /* @__PURE__ */ new Set();
7861
+ for (const record4 of records) {
7862
+ if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text4(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
7863
+ const key = recordKey(record4);
7864
+ if (seen.has(key)) return false;
7865
+ seen.add(key);
7866
+ }
7867
+ return Number.isSafeInteger(snapshot.counts.scanned) && Number.isSafeInteger(snapshot.counts.eligible) && Number.isSafeInteger(snapshot.counts.returned) && snapshot.counts.returned === records.length && snapshot.counts.scanned >= snapshot.counts.eligible && snapshot.counts.eligible >= snapshot.counts.returned;
7868
+ }
7869
+ async function reconcileCommercialParitySnapshot(input) {
7870
+ if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
7871
+ const sourceKey = networkCommercialSourceKey(input.source);
7872
+ const key = await parityKey(input.secret);
7873
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text4(row.applicationId) && text4(row.objectId)).map(async (row) => {
7874
+ const customerId = text4(row.customerId);
7875
+ const subscriptionId = text4(row.subscriptionId) ?? text4(row.objectId);
7876
+ return {
7877
+ applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
7878
+ customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
7879
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
7880
+ status: null,
7881
+ canceled: false,
7882
+ stripeStatus: text4(row.status, 64),
7883
+ stripeCanceled: row.status === "canceled"
7884
+ };
7885
+ }));
7886
+ const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
7887
+ const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
7888
+ const exceptions = [];
7889
+ const records = [];
7890
+ for (const [key2, chapter] of chapterByKey) {
7891
+ const stripe = shadowByKey.get(key2);
7892
+ if (!stripe) {
7893
+ exceptions.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
7894
+ continue;
7895
+ }
7896
+ const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
7897
+ records.push(compared);
7898
+ if (chapter.canceled !== stripe.stripeCanceled) {
7899
+ exceptions.push({
7900
+ kind: "cancellation_mismatch",
7901
+ applicationDigest: chapter.applicationDigest,
7902
+ detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
7903
+ });
7904
+ }
7905
+ }
7906
+ for (const [key2, stripe] of shadowByKey) {
7907
+ if (!chapterByKey.has(key2)) {
7908
+ exceptions.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
7909
+ }
7910
+ }
7911
+ if (!input.snapshot.complete || input.snapshot.truncated) {
7912
+ exceptions.push({
7913
+ kind: "chapter_truncated",
7914
+ detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
7915
+ });
7916
+ }
7917
+ if (chapterByKey.size !== shadowByKey.size) {
7918
+ exceptions.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
7919
+ }
7920
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
7921
+ return {
7922
+ ok: input.snapshot.complete && exceptions.length === 0,
7923
+ complete: input.snapshot.complete,
7924
+ counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
7925
+ records,
7926
+ exceptions
7927
+ };
7928
+ }
7929
+
7930
+ // src/network-commercial-parity-response.ts
7931
+ var MAX_RESPONSE_BYTES = 512e3;
7932
+ async function readBoundedCommercialParityResponse(response) {
7933
+ const declared = Number(response.headers.get("content-length") ?? 0);
7934
+ if (declared > MAX_RESPONSE_BYTES) return null;
7935
+ if (!response.body) return "";
7936
+ const reader = response.body.getReader();
7937
+ const chunks = [];
7938
+ let total = 0;
7939
+ for (; ; ) {
7940
+ const next = await reader.read();
7941
+ if (next.done) break;
7942
+ total += next.value.byteLength;
7943
+ if (total > MAX_RESPONSE_BYTES) {
7944
+ await reader.cancel();
7945
+ return null;
7946
+ }
7947
+ chunks.push(next.value);
7948
+ }
7949
+ const body = new Uint8Array(total);
7950
+ let offset = 0;
7951
+ for (const chunk of chunks) {
7952
+ body.set(chunk, offset);
7953
+ offset += chunk.byteLength;
7954
+ }
7955
+ return new TextDecoder().decode(body);
7956
+ }
7957
+
7958
+ // src/worker-routes-network-commercial-parity.ts
7959
+ var PARITY_PATH = "/api/network/commercial-parity";
7960
+ var PAGE2 = 500;
7961
+ var MAX_SCAN = 5e3;
7962
+ var MAX_RECORDS2 = 500;
7963
+ async function localCommercialRows(db) {
7964
+ const rows6 = [];
7965
+ for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
7966
+ const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
7967
+ const page = Array.isArray(result.applications) ? result.applications : [];
7968
+ rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
7969
+ if (page.length < PAGE2) return { rows: rows6, truncated: false };
7970
+ }
7971
+ return { rows: rows6, truncated: true };
7972
+ }
7973
+ var handleNetworkCommercialParity = async (req, url, env, ctx) => {
7974
+ if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
7975
+ if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
7976
+ const readers = ctx.chapter.network.readers.filter((reader) => reader.commercialParity);
7977
+ if (readers.length === 0) return json({ error: "unauthorized" }, 401);
7978
+ const db = ctx.makeDb(env);
7979
+ const secret = await getVaultSecret(db, "network_share_secret");
7980
+ if (!secret) return json({ error: "unauthorized" }, 401);
7981
+ const verified = await verifyFederatedRequest8(req, { secret, senders: readers.map((reader) => reader.id) });
7982
+ if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
7983
+ const partition = {
7984
+ appId: env.ODLA_APP_ID,
7985
+ chapterId: ctx.chapter.id,
7986
+ environment: env.ODLA_ENV,
7987
+ dataEnvironment: env.ODLA_ENV,
7988
+ runtime: env.ODLA_RUNTIME?.trim() || "default"
7989
+ };
7990
+ try {
7991
+ const local3 = await localCommercialRows(db);
7992
+ return json(await buildChapterCommercialParitySnapshot({
7993
+ rows: local3.rows,
7994
+ secret,
7995
+ partition,
7996
+ siteId: ctx.chapter.id,
7997
+ scanned: local3.rows.length,
7998
+ scanTruncated: local3.truncated,
7999
+ maxRecords: MAX_RECORDS2
8000
+ }));
8001
+ } catch {
8002
+ return json({ error: "commercial parity unavailable" }, 503);
8003
+ }
8004
+ };
8005
+ async function reconcileTarget(db, env, ctx, target, projection) {
8006
+ const base = { targetId: target.id, name: target.name, url: target.url };
8007
+ const secret = await getVaultSecret(db, target.secretName);
8008
+ if (!secret) return { ...base, available: false, ok: false, error: { code: "edge_secret_missing" } };
8009
+ const destination = new URL(PARITY_PATH, target.url);
8010
+ try {
8011
+ const transport = resolveNetworkTargetTransport(env, target);
8012
+ const headers = await signFederatedRequest6({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
8013
+ const response = await transport.fetch(destination, { headers, signal: AbortSignal.timeout(1e4) });
8014
+ const responseBody = await readBoundedCommercialParityResponse(response);
8015
+ const snapshot = responseBody !== null ? (() => {
8016
+ try {
8017
+ return JSON.parse(responseBody);
8018
+ } catch {
8019
+ return null;
8020
+ }
8021
+ })() : null;
8022
+ if (!response.ok || !snapshot) {
8023
+ return {
8024
+ ...base,
8025
+ available: false,
8026
+ ok: false,
8027
+ transport: transport.kind,
8028
+ error: { code: "chapter_response_failed", status: response.status }
8029
+ };
8030
+ }
8031
+ const sources = ctx.chapter.networkCommercialReadModel?.sources ?? [];
8032
+ const matchingSources = sources.filter((candidate) => candidate.appId === snapshot.partition?.appId && candidate.chapterId === snapshot.partition?.chapterId && candidate.environment === snapshot.partition?.environment && candidate.dataEnvironment === snapshot.partition?.dataEnvironment && candidate.runtime === snapshot.partition?.runtime);
8033
+ if (matchingSources.length !== 1) {
8034
+ return {
8035
+ ...base,
8036
+ available: true,
8037
+ ok: false,
8038
+ transport: transport.kind,
8039
+ error: { code: matchingSources.length === 0 ? "source_not_configured" : "source_ambiguous" }
8040
+ };
8041
+ }
8042
+ return {
8043
+ ...base,
8044
+ available: true,
8045
+ transport: transport.kind,
8046
+ generatedAt: snapshot.generatedAt,
8047
+ ...await reconcileCommercialParitySnapshot({
8048
+ projection,
8049
+ source: matchingSources[0],
8050
+ snapshot,
8051
+ secret
8052
+ })
8053
+ };
8054
+ } catch (error) {
8055
+ return {
8056
+ ...base,
8057
+ available: false,
8058
+ ok: false,
8059
+ error: {
8060
+ code: error instanceof NetworkTargetTransportError ? "binding_unavailable" : error instanceof Error && error.message === "invalid chapter commercial parity snapshot" ? "invalid_chapter_snapshot" : "chapter_request_failed"
8061
+ }
8062
+ };
8063
+ }
8064
+ }
8065
+ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
8066
+ const targets = ctx.chapter.network.targets.filter((target) => target.commercialParity);
8067
+ const results = await Promise.all(targets.map((target) => reconcileTarget(db, env, ctx, target, projection)));
8068
+ return {
8069
+ reconciled: true,
8070
+ provider: "chapters",
8071
+ writable: false,
8072
+ configured: targets.length,
8073
+ available: results.filter((result) => result.available).length,
8074
+ ok: results.every((result) => result.ok),
8075
+ targets: results
8076
+ };
8077
+ }
8078
+
7781
8079
  // src/worker-routes-network-commercial.ts
7782
8080
  var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
7783
8081
  var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
@@ -7881,6 +8179,10 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
7881
8179
  } catch {
7882
8180
  return json({ error: "invalid JSON body" }, 400);
7883
8181
  }
8182
+ if (parsed.command === "reconcile-chapters") {
8183
+ const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
8184
+ return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
8185
+ }
7884
8186
  if (parsed.command === "reconcile-stripe") {
7885
8187
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
7886
8188
  if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
@@ -7903,7 +8205,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
7903
8205
  }
7904
8206
  }
7905
8207
  if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
7906
- return json({ error: "rebuild or reconcile-stripe command is required; rebuild also requires a valid mutationId" }, 400);
8208
+ return json({ error: "rebuild, reconcile-stripe, or reconcile-chapters command is required; rebuild also requires a valid mutationId" }, 400);
7907
8209
  }
7908
8210
  const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
7909
8211
  return json({ rebuilt: true, ...safeProjection(projection) });
@@ -7992,7 +8294,7 @@ var handleFormation = async (req, url, env, ctx) => {
7992
8294
  };
7993
8295
 
7994
8296
  // src/worker-routes-builder-library.ts
7995
- import { verifyFederatedRequest as verifyFederatedRequest8 } from "@odla-ai/db";
8297
+ import { verifyFederatedRequest as verifyFederatedRequest9 } from "@odla-ai/db";
7996
8298
 
7997
8299
  // src/builder-library-validation.ts
7998
8300
  var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -8015,7 +8317,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
8015
8317
  function unknownKeys3(value, allowed2, path, errors) {
8016
8318
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
8017
8319
  }
8018
- function text4(value, path, errors, max = MAX_TEXT2, required = true) {
8320
+ function text5(value, path, errors, max = MAX_TEXT2, required = true) {
8019
8321
  if (value === void 0 && !required) return void 0;
8020
8322
  if (typeof value !== "string" || required && !value.trim()) {
8021
8323
  errors.push(`${path} must be a non-empty string`);
@@ -8041,7 +8343,7 @@ function scopeValue(value, path, errors) {
8041
8343
  return { kind: raw2.kind };
8042
8344
  }
8043
8345
  if (raw2.kind === "chapter") {
8044
- const chapterId = text4(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
8346
+ const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
8045
8347
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
8046
8348
  return { kind: "chapter", chapterId };
8047
8349
  }
@@ -8055,22 +8357,22 @@ function resourceValue(value, path, errors) {
8055
8357
  return null;
8056
8358
  }
8057
8359
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
8058
- const id = text4(raw2.id, `${path}.id`, errors, 80) ?? "";
8360
+ const id = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
8059
8361
  if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
8060
- const kind = text4(raw2.kind, `${path}.kind`, errors) ?? "";
8362
+ const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
8061
8363
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
8062
- const title = text4(raw2.title, `${path}.title`, errors, 240) ?? "";
8063
- const summary = text4(raw2.summary, `${path}.summary`, errors, 1e3, false);
8064
- const status = text4(raw2.status, `${path}.status`, errors) ?? "";
8364
+ const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
8365
+ const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
8366
+ const status = text5(raw2.status, `${path}.status`, errors) ?? "";
8065
8367
  if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
8066
8368
  const revision = integer3(raw2.revision, `${path}.revision`, errors);
8067
8369
  const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
8068
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text4(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
8370
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
8069
8371
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
8070
8372
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
8071
8373
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
8072
- const bodyMarkdown = text4(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
8073
- const url = text4(raw2.url, `${path}.url`, errors, 2048, false);
8374
+ const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
8375
+ const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
8074
8376
  if (url) {
8075
8377
  try {
8076
8378
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -8082,9 +8384,9 @@ function resourceValue(value, path, errors) {
8082
8384
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
8083
8385
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
8084
8386
  const brandAsset = asset ? {
8085
- bookId: text4(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
8086
- assetId: text4(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
8087
- ...asset.filename === void 0 ? {} : { filename: text4(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
8387
+ bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
8388
+ assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
8389
+ ...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
8088
8390
  } : void 0;
8089
8391
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
8090
8392
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -8099,17 +8401,17 @@ function resourceValue(value, path, errors) {
8099
8401
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
8100
8402
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
8101
8403
  const promotedFrom = promotedRaw ? {
8102
- chapterId: text4(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
8103
- resourceId: text4(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
8404
+ chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
8405
+ resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
8104
8406
  resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
8105
8407
  documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
8106
- promotedBy: text4(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
8408
+ promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
8107
8409
  promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
8108
8410
  } : void 0;
8109
8411
  const provenance = {
8110
- createdBy: text4(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
8412
+ createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
8111
8413
  createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
8112
- updatedBy: text4(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
8414
+ updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
8113
8415
  updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
8114
8416
  ...promotedFrom ? { promotedFrom } : {}
8115
8417
  };
@@ -8128,7 +8430,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
8128
8430
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
8129
8431
  const sourceType = sourceRaw?.type;
8130
8432
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
8131
- const sourceAppId = text4(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
8433
+ const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
8132
8434
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
8133
8435
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
8134
8436
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -8136,8 +8438,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
8136
8438
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
8137
8439
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
8138
8440
  const target = targetRaw ? {
8139
- appId: text4(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
8140
- environment: text4(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
8441
+ appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
8442
+ environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
8141
8443
  } : void 0;
8142
8444
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
8143
8445
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -8275,7 +8577,7 @@ async function verifySource(req, db, ctx) {
8275
8577
  if (ctx.chapter.mode === "chapter") {
8276
8578
  const secret = await getVaultSecret(db, "network_share_secret");
8277
8579
  if (!secret) return null;
8278
- const verified = await verifyFederatedRequest8(req, {
8580
+ const verified = await verifyFederatedRequest9(req, {
8279
8581
  secret,
8280
8582
  senders: sources.map((source2) => source2.sourceId)
8281
8583
  });
@@ -8288,7 +8590,7 @@ async function verifySource(req, db, ctx) {
8288
8590
  if (!target) continue;
8289
8591
  const secret = await getVaultSecret(db, target.secretName);
8290
8592
  if (!secret) continue;
8291
- const verified = await verifyFederatedRequest8(req.clone(), { secret, senders: [source.sourceId] });
8593
+ const verified = await verifyFederatedRequest9(req.clone(), { secret, senders: [source.sourceId] });
8292
8594
  if (verified.ok) return { source, body: verified.body };
8293
8595
  }
8294
8596
  return null;
@@ -8478,11 +8780,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
8478
8780
  };
8479
8781
 
8480
8782
  // src/signup-control-delivery.ts
8481
- import { signFederatedRequest as signFederatedRequest6 } from "@odla-ai/db";
8783
+ import { signFederatedRequest as signFederatedRequest7 } from "@odla-ai/db";
8482
8784
  async function deliverSignupControl(options) {
8483
8785
  const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
8484
8786
  const body = JSON.stringify(options.envelope);
8485
- const signed = await signFederatedRequest6({
8787
+ const signed = await signFederatedRequest7({
8486
8788
  secret: options.secret,
8487
8789
  sender: options.sender,
8488
8790
  method: "POST",
@@ -8503,11 +8805,11 @@ async function deliverSignupControl(options) {
8503
8805
  }
8504
8806
 
8505
8807
  // src/member-content-delivery.ts
8506
- import { signFederatedRequest as signFederatedRequest7 } from "@odla-ai/db";
8808
+ import { signFederatedRequest as signFederatedRequest8 } from "@odla-ai/db";
8507
8809
  async function deliverMemberContent(options) {
8508
8810
  const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
8509
8811
  const body = JSON.stringify(options.envelope);
8510
- const signed = await signFederatedRequest7({
8812
+ const signed = await signFederatedRequest8({
8511
8813
  secret: options.secret,
8512
8814
  sender: options.sender,
8513
8815
  method: "POST",
@@ -8528,11 +8830,11 @@ async function deliverMemberContent(options) {
8528
8830
  }
8529
8831
 
8530
8832
  // src/builder-library-delivery.ts
8531
- import { signFederatedRequest as signFederatedRequest8 } from "@odla-ai/db";
8833
+ import { signFederatedRequest as signFederatedRequest9 } from "@odla-ai/db";
8532
8834
  async function deliverBuilderLibrary(options) {
8533
8835
  const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
8534
8836
  const body = JSON.stringify(options.envelope);
8535
- const signed = await signFederatedRequest8({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
8837
+ const signed = await signFederatedRequest9({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
8536
8838
  const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
8537
8839
  const result = await response.json();
8538
8840
  if (!response.ok || !("ok" in result) || result.ok !== true) {
@@ -8546,7 +8848,7 @@ async function deliverBuilderLibrary(options) {
8546
8848
  var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
8547
8849
  var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
8548
8850
  var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
8549
- var DIGEST = /^sha256:[0-9a-f]{64}$/;
8851
+ var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
8550
8852
  var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
8551
8853
  var MAX_TOKENS = 256;
8552
8854
  var MAX_VALUE_LENGTH = 1e3;
@@ -8590,7 +8892,7 @@ function stateFor(revision, options) {
8590
8892
  metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
8591
8893
  };
8592
8894
  }
8593
- if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
8895
+ if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
8594
8896
  throw new TypeError("signup-control theme revision metadata is invalid");
8595
8897
  }
8596
8898
  return {
@@ -8687,6 +8989,7 @@ var BUILTIN_ROUTES = [
8687
8989
  handleNetworkSnapshot,
8688
8990
  handleNetworkRecords,
8689
8991
  handleNetworkSharedNotes,
8992
+ handleNetworkCommercialParity,
8690
8993
  handleNetworkCommercialStripeWebhook,
8691
8994
  handleFormation,
8692
8995
  handleMember,