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