@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.
@@ -759,8 +759,8 @@ var APPLICATION_LABELS = {
759
759
  state: "State"
760
760
  };
761
761
  function humanize(id) {
762
- const text5 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
763
- return text5 ? `${text5[0].toUpperCase()}${text5.slice(1)}` : id;
762
+ const text6 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
763
+ return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id;
764
764
  }
765
765
  function applicationDefs(chapter) {
766
766
  try {
@@ -1290,10 +1290,10 @@ function planDelivery(input) {
1290
1290
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1291
1291
  const to = redirect ? input.group.debugEmail : input.to;
1292
1292
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1293
- const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
1293
+ const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
1294
1294
 
1295
1295
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1296
- return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
1296
+ return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
1297
1297
  }
1298
1298
 
1299
1299
  // src/notify.ts
@@ -2181,10 +2181,10 @@ function boundedString(value, path, errors, max, required = false) {
2181
2181
  errors.push(`${path} must be a string`);
2182
2182
  return void 0;
2183
2183
  }
2184
- const text5 = value.trim();
2185
- if (required && !text5) errors.push(`${path} is required`);
2186
- if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2187
- return text5;
2184
+ const text6 = value.trim();
2185
+ if (required && !text6) errors.push(`${path} is required`);
2186
+ if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2187
+ return text6;
2188
2188
  }
2189
2189
  function validateMembershipPageDocument(input, options = {}) {
2190
2190
  const errors = [];
@@ -2233,8 +2233,8 @@ function validateMembershipPageDocument(input, options = {}) {
2233
2233
  else {
2234
2234
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
2235
2235
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
2236
- const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2237
- return text5 ? [text5] : [];
2236
+ const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2237
+ return text6 ? [text6] : [];
2238
2238
  });
2239
2239
  }
2240
2240
  }
@@ -3946,17 +3946,17 @@ async function notifyAdminOfPayment(db, env, eventId, app) {
3946
3946
  try {
3947
3947
  const group = await groupFor(db, app);
3948
3948
  if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
3949
- const text5 = (value) => typeof value === "string" ? value : "";
3949
+ const text6 = (value) => typeof value === "string" ? value : "";
3950
3950
  await sendTemplated(deps(db, env), {
3951
3951
  group: emailGroupFrom(group),
3952
3952
  template: "adminNotification",
3953
3953
  to: group.notificationEmail,
3954
3954
  vars: {
3955
- firstName: text5(app.firstName),
3956
- lastName: text5(app.lastName),
3957
- email: text5(app.email),
3958
- phone: text5(app.phone),
3959
- state: text5(app.state)
3955
+ firstName: text6(app.firstName),
3956
+ lastName: text6(app.lastName),
3957
+ email: text6(app.email),
3958
+ phone: text6(app.phone),
3959
+ state: text6(app.state)
3960
3960
  },
3961
3961
  dedupeKey: `${eventId}:admin`,
3962
3962
  applicationId: String(app.id)
@@ -5720,11 +5720,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
5720
5720
  for (const key of EMAIL_TEMPLATE_NAMES) {
5721
5721
  const t = templates[key];
5722
5722
  const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
5723
- const text5 = typeof t?.text === "string" ? t.text : "";
5724
- if (!subject || !text5.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
5723
+ const text6 = typeof t?.text === "string" ? t.text : "";
5724
+ if (!subject || !text6.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
5725
5725
  if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
5726
- if (text5.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
5727
- clean[key] = { subject, text: text5, enabled: t?.enabled !== false };
5726
+ if (text6.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
5727
+ clean[key] = { subject, text: text6, enabled: t?.enabled !== false };
5728
5728
  }
5729
5729
  const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5730
5730
  const notificationEmail = str2(body.notificationEmail).trim();
@@ -5941,7 +5941,8 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
5941
5941
  fields,
5942
5942
  sharedNotes,
5943
5943
  editableFields,
5944
- stageTransitions
5944
+ stageTransitions,
5945
+ commercialParity
5945
5946
  }) => ({
5946
5947
  id,
5947
5948
  name,
@@ -5949,7 +5950,8 @@ var handleAdminNetworkTargets = async (req, url, env, ctx) => {
5949
5950
  types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
5950
5951
  sharedNotes,
5951
5952
  editableFields,
5952
- stageTransitions
5953
+ stageTransitions,
5954
+ commercialParity
5953
5955
  }))
5954
5956
  });
5955
5957
  };
@@ -7207,9 +7209,9 @@ var handleAdminNetworkPersonRegistry = async (req, url, env, ctx) => {
7207
7209
  if (req.method === "POST") {
7208
7210
  let body;
7209
7211
  try {
7210
- const text5 = await req.text();
7211
- if (new TextEncoder().encode(text5).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
7212
- const parsed = JSON.parse(text5);
7212
+ const text6 = await req.text();
7213
+ if (new TextEncoder().encode(text6).byteLength > MAX_BODY2) return json({ error: "request is too large" }, 413);
7214
+ const parsed = JSON.parse(text6);
7213
7215
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid");
7214
7216
  body = parsed;
7215
7217
  } catch {
@@ -7804,6 +7806,298 @@ function reconcileStripeSnapshot(shadow, snapshots, sources) {
7804
7806
  };
7805
7807
  }
7806
7808
 
7809
+ // src/worker-routes-network-commercial-parity.ts
7810
+ var import_db12 = require("@odla-ai/db");
7811
+
7812
+ // src/network-commercial-parity.ts
7813
+ var DIGEST = /^[0-9a-f]{64}$/;
7814
+ var MAX_RECORDS = 500;
7815
+ var text4 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
7816
+ function parityMessage(partition, kind, value) {
7817
+ return [
7818
+ "chapter-commercial-parity",
7819
+ "v1",
7820
+ partition.appId,
7821
+ partition.chapterId,
7822
+ partition.environment,
7823
+ partition.dataEnvironment,
7824
+ partition.runtime,
7825
+ kind,
7826
+ value
7827
+ ].join("");
7828
+ }
7829
+ async function parityKey(secret) {
7830
+ const encoder4 = new TextEncoder();
7831
+ return crypto.subtle.importKey(
7832
+ "raw",
7833
+ encoder4.encode(secret),
7834
+ { name: "HMAC", hash: "SHA-256" },
7835
+ false,
7836
+ ["sign"]
7837
+ );
7838
+ }
7839
+ async function digestWithKey(key, partition, kind, value) {
7840
+ const encoder4 = new TextEncoder();
7841
+ const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
7842
+ return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
7843
+ }
7844
+ async function buildChapterCommercialParitySnapshot(input) {
7845
+ const requestedMax = input.maxRecords ?? MAX_RECORDS;
7846
+ const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
7847
+ const eligible = input.rows.filter((row) => text4(row.id) && (text4(row.stripeCustomerId) || text4(row.stripeSubscriptionId)));
7848
+ const selected2 = eligible.slice(0, maxRecords);
7849
+ const key = await parityKey(input.secret);
7850
+ const records = await Promise.all(selected2.map(async (row) => {
7851
+ const applicationId = text4(row.id);
7852
+ const customerId = text4(row.stripeCustomerId);
7853
+ const subscriptionId = text4(row.stripeSubscriptionId);
7854
+ return {
7855
+ applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
7856
+ customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
7857
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
7858
+ status: text4(row.status, 64),
7859
+ canceled: row.canceled === true
7860
+ };
7861
+ }));
7862
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
7863
+ const truncated = input.scanTruncated || eligible.length > maxRecords;
7864
+ return {
7865
+ version: 1,
7866
+ site: { id: input.siteId },
7867
+ partition: input.partition,
7868
+ generatedAt: input.now ?? Date.now(),
7869
+ complete: !truncated,
7870
+ truncated,
7871
+ counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
7872
+ records
7873
+ };
7874
+ }
7875
+ function recordKey(record4) {
7876
+ return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
7877
+ }
7878
+ function validSnapshot(snapshot, source) {
7879
+ const partition = snapshot?.partition;
7880
+ const records = snapshot?.records;
7881
+ 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;
7882
+ const seen = /* @__PURE__ */ new Set();
7883
+ for (const record4 of records) {
7884
+ 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;
7885
+ const key = recordKey(record4);
7886
+ if (seen.has(key)) return false;
7887
+ seen.add(key);
7888
+ }
7889
+ 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;
7890
+ }
7891
+ async function reconcileCommercialParitySnapshot(input) {
7892
+ if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
7893
+ const sourceKey = networkCommercialSourceKey(input.source);
7894
+ const key = await parityKey(input.secret);
7895
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text4(row.applicationId) && text4(row.objectId)).map(async (row) => {
7896
+ const customerId = text4(row.customerId);
7897
+ const subscriptionId = text4(row.subscriptionId) ?? text4(row.objectId);
7898
+ return {
7899
+ applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
7900
+ customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
7901
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
7902
+ status: null,
7903
+ canceled: false,
7904
+ stripeStatus: text4(row.status, 64),
7905
+ stripeCanceled: row.status === "canceled"
7906
+ };
7907
+ }));
7908
+ const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
7909
+ const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
7910
+ const exceptions = [];
7911
+ const records = [];
7912
+ for (const [key2, chapter] of chapterByKey) {
7913
+ const stripe = shadowByKey.get(key2);
7914
+ if (!stripe) {
7915
+ exceptions.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
7916
+ continue;
7917
+ }
7918
+ const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
7919
+ records.push(compared);
7920
+ if (chapter.canceled !== stripe.stripeCanceled) {
7921
+ exceptions.push({
7922
+ kind: "cancellation_mismatch",
7923
+ applicationDigest: chapter.applicationDigest,
7924
+ detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
7925
+ });
7926
+ }
7927
+ }
7928
+ for (const [key2, stripe] of shadowByKey) {
7929
+ if (!chapterByKey.has(key2)) {
7930
+ exceptions.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
7931
+ }
7932
+ }
7933
+ if (!input.snapshot.complete || input.snapshot.truncated) {
7934
+ exceptions.push({
7935
+ kind: "chapter_truncated",
7936
+ detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
7937
+ });
7938
+ }
7939
+ if (chapterByKey.size !== shadowByKey.size) {
7940
+ exceptions.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
7941
+ }
7942
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
7943
+ return {
7944
+ ok: input.snapshot.complete && exceptions.length === 0,
7945
+ complete: input.snapshot.complete,
7946
+ counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
7947
+ records,
7948
+ exceptions
7949
+ };
7950
+ }
7951
+
7952
+ // src/network-commercial-parity-response.ts
7953
+ var MAX_RESPONSE_BYTES = 512e3;
7954
+ async function readBoundedCommercialParityResponse(response) {
7955
+ const declared = Number(response.headers.get("content-length") ?? 0);
7956
+ if (declared > MAX_RESPONSE_BYTES) return null;
7957
+ if (!response.body) return "";
7958
+ const reader = response.body.getReader();
7959
+ const chunks = [];
7960
+ let total = 0;
7961
+ for (; ; ) {
7962
+ const next = await reader.read();
7963
+ if (next.done) break;
7964
+ total += next.value.byteLength;
7965
+ if (total > MAX_RESPONSE_BYTES) {
7966
+ await reader.cancel();
7967
+ return null;
7968
+ }
7969
+ chunks.push(next.value);
7970
+ }
7971
+ const body = new Uint8Array(total);
7972
+ let offset = 0;
7973
+ for (const chunk of chunks) {
7974
+ body.set(chunk, offset);
7975
+ offset += chunk.byteLength;
7976
+ }
7977
+ return new TextDecoder().decode(body);
7978
+ }
7979
+
7980
+ // src/worker-routes-network-commercial-parity.ts
7981
+ var PARITY_PATH = "/api/network/commercial-parity";
7982
+ var PAGE2 = 500;
7983
+ var MAX_SCAN = 5e3;
7984
+ var MAX_RECORDS2 = 500;
7985
+ async function localCommercialRows(db) {
7986
+ const rows6 = [];
7987
+ for (let offset = 0; offset < MAX_SCAN; offset += PAGE2) {
7988
+ const result = await db.query({ applications: { $: { limit: PAGE2, offset, order: { createdAt: "asc" } } } });
7989
+ const page = Array.isArray(result.applications) ? result.applications : [];
7990
+ rows6.push(...page.slice(0, MAX_SCAN - rows6.length));
7991
+ if (page.length < PAGE2) return { rows: rows6, truncated: false };
7992
+ }
7993
+ return { rows: rows6, truncated: true };
7994
+ }
7995
+ var handleNetworkCommercialParity = async (req, url, env, ctx) => {
7996
+ if (req.method !== "GET" || url.pathname !== PARITY_PATH) return null;
7997
+ if (ctx.chapter.mode !== "chapter") return json({ error: "not found" }, 404);
7998
+ const readers = ctx.chapter.network.readers.filter((reader) => reader.commercialParity);
7999
+ if (readers.length === 0) return json({ error: "unauthorized" }, 401);
8000
+ const db = ctx.makeDb(env);
8001
+ const secret = await getVaultSecret(db, "network_share_secret");
8002
+ if (!secret) return json({ error: "unauthorized" }, 401);
8003
+ const verified = await (0, import_db12.verifyFederatedRequest)(req, { secret, senders: readers.map((reader) => reader.id) });
8004
+ if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
8005
+ const partition = {
8006
+ appId: env.ODLA_APP_ID,
8007
+ chapterId: ctx.chapter.id,
8008
+ environment: env.ODLA_ENV,
8009
+ dataEnvironment: env.ODLA_ENV,
8010
+ runtime: env.ODLA_RUNTIME?.trim() || "default"
8011
+ };
8012
+ try {
8013
+ const local3 = await localCommercialRows(db);
8014
+ return json(await buildChapterCommercialParitySnapshot({
8015
+ rows: local3.rows,
8016
+ secret,
8017
+ partition,
8018
+ siteId: ctx.chapter.id,
8019
+ scanned: local3.rows.length,
8020
+ scanTruncated: local3.truncated,
8021
+ maxRecords: MAX_RECORDS2
8022
+ }));
8023
+ } catch {
8024
+ return json({ error: "commercial parity unavailable" }, 503);
8025
+ }
8026
+ };
8027
+ async function reconcileTarget(db, env, ctx, target, projection) {
8028
+ const base = { targetId: target.id, name: target.name, url: target.url };
8029
+ const secret = await getVaultSecret(db, target.secretName);
8030
+ if (!secret) return { ...base, available: false, ok: false, error: { code: "edge_secret_missing" } };
8031
+ const destination = new URL(PARITY_PATH, target.url);
8032
+ try {
8033
+ const transport = resolveNetworkTargetTransport(env, target);
8034
+ const headers = await (0, import_db12.signFederatedRequest)({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
8035
+ const response = await transport.fetch(destination, { headers, signal: AbortSignal.timeout(1e4) });
8036
+ const responseBody = await readBoundedCommercialParityResponse(response);
8037
+ const snapshot = responseBody !== null ? (() => {
8038
+ try {
8039
+ return JSON.parse(responseBody);
8040
+ } catch {
8041
+ return null;
8042
+ }
8043
+ })() : null;
8044
+ if (!response.ok || !snapshot) {
8045
+ return {
8046
+ ...base,
8047
+ available: false,
8048
+ ok: false,
8049
+ transport: transport.kind,
8050
+ error: { code: "chapter_response_failed", status: response.status }
8051
+ };
8052
+ }
8053
+ const sources = ctx.chapter.networkCommercialReadModel?.sources ?? [];
8054
+ 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);
8055
+ if (matchingSources.length !== 1) {
8056
+ return {
8057
+ ...base,
8058
+ available: true,
8059
+ ok: false,
8060
+ transport: transport.kind,
8061
+ error: { code: matchingSources.length === 0 ? "source_not_configured" : "source_ambiguous" }
8062
+ };
8063
+ }
8064
+ return {
8065
+ ...base,
8066
+ available: true,
8067
+ transport: transport.kind,
8068
+ generatedAt: snapshot.generatedAt,
8069
+ ...await reconcileCommercialParitySnapshot({
8070
+ projection,
8071
+ source: matchingSources[0],
8072
+ snapshot,
8073
+ secret
8074
+ })
8075
+ };
8076
+ } catch (error) {
8077
+ return {
8078
+ ...base,
8079
+ available: false,
8080
+ ok: false,
8081
+ error: {
8082
+ code: error instanceof NetworkTargetTransportError ? "binding_unavailable" : error instanceof Error && error.message === "invalid chapter commercial parity snapshot" ? "invalid_chapter_snapshot" : "chapter_request_failed"
8083
+ }
8084
+ };
8085
+ }
8086
+ }
8087
+ async function reconcileChapterCommercialTargets(db, env, ctx, projection) {
8088
+ const targets = ctx.chapter.network.targets.filter((target) => target.commercialParity);
8089
+ const results = await Promise.all(targets.map((target) => reconcileTarget(db, env, ctx, target, projection)));
8090
+ return {
8091
+ reconciled: true,
8092
+ provider: "chapters",
8093
+ writable: false,
8094
+ configured: targets.length,
8095
+ available: results.filter((result) => result.available).length,
8096
+ ok: results.every((result) => result.ok),
8097
+ targets: results
8098
+ };
8099
+ }
8100
+
7807
8101
  // src/worker-routes-network-commercial.ts
7808
8102
  var ADMIN_PATH2 = "/api/admin/network-commercial-read-model";
7809
8103
  var WEBHOOK_PATH = "/api/network-commercial/stripe-webhook";
@@ -7907,6 +8201,10 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
7907
8201
  } catch {
7908
8202
  return json({ error: "invalid JSON body" }, 400);
7909
8203
  }
8204
+ if (parsed.command === "reconcile-chapters") {
8205
+ const projection2 = await loadNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources);
8206
+ return json(await reconcileChapterCommercialTargets(db, env, ctx, projection2));
8207
+ }
7910
8208
  if (parsed.command === "reconcile-stripe") {
7911
8209
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
7912
8210
  if (!secretKey) return json({ error: "Stripe reconciliation is not configured" }, 503);
@@ -7929,7 +8227,7 @@ var handleAdminNetworkCommercialReadModel = async (req, url, env, ctx) => {
7929
8227
  }
7930
8228
  }
7931
8229
  if (parsed.command !== "rebuild" || typeof parsed.mutationId !== "string" || !MUTATION_ID5.test(parsed.mutationId)) {
7932
- return json({ error: "rebuild or reconcile-stripe command is required; rebuild also requires a valid mutationId" }, 400);
8230
+ return json({ error: "rebuild, reconcile-stripe, or reconcile-chapters command is required; rebuild also requires a valid mutationId" }, 400);
7933
8231
  }
7934
8232
  const projection = await rebuildNetworkCommercialProjection(db, ctx.chapter.networkCommercialReadModel.sources, parsed.mutationId);
7935
8233
  return json({ rebuilt: true, ...safeProjection(projection) });
@@ -8018,7 +8316,7 @@ var handleFormation = async (req, url, env, ctx) => {
8018
8316
  };
8019
8317
 
8020
8318
  // src/worker-routes-builder-library.ts
8021
- var import_db12 = require("@odla-ai/db");
8319
+ var import_db13 = require("@odla-ai/db");
8022
8320
 
8023
8321
  // src/builder-library-validation.ts
8024
8322
  var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -8041,7 +8339,7 @@ var record3 = (value) => value !== null && typeof value === "object" && !Array.i
8041
8339
  function unknownKeys3(value, allowed2, path, errors) {
8042
8340
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
8043
8341
  }
8044
- function text4(value, path, errors, max = MAX_TEXT2, required = true) {
8342
+ function text5(value, path, errors, max = MAX_TEXT2, required = true) {
8045
8343
  if (value === void 0 && !required) return void 0;
8046
8344
  if (typeof value !== "string" || required && !value.trim()) {
8047
8345
  errors.push(`${path} must be a non-empty string`);
@@ -8067,7 +8365,7 @@ function scopeValue(value, path, errors) {
8067
8365
  return { kind: raw2.kind };
8068
8366
  }
8069
8367
  if (raw2.kind === "chapter") {
8070
- const chapterId = text4(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
8368
+ const chapterId = text5(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
8071
8369
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
8072
8370
  return { kind: "chapter", chapterId };
8073
8371
  }
@@ -8081,22 +8379,22 @@ function resourceValue(value, path, errors) {
8081
8379
  return null;
8082
8380
  }
8083
8381
  unknownKeys3(raw2, RESOURCE_KEYS, path, errors);
8084
- const id = text4(raw2.id, `${path}.id`, errors, 80) ?? "";
8382
+ const id = text5(raw2.id, `${path}.id`, errors, 80) ?? "";
8085
8383
  if (id && !ID4.test(id)) errors.push(`${path}.id must be lowercase kebab-case`);
8086
- const kind = text4(raw2.kind, `${path}.kind`, errors) ?? "";
8384
+ const kind = text5(raw2.kind, `${path}.kind`, errors) ?? "";
8087
8385
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
8088
- const title = text4(raw2.title, `${path}.title`, errors, 240) ?? "";
8089
- const summary = text4(raw2.summary, `${path}.summary`, errors, 1e3, false);
8090
- const status = text4(raw2.status, `${path}.status`, errors) ?? "";
8386
+ const title = text5(raw2.title, `${path}.title`, errors, 240) ?? "";
8387
+ const summary = text5(raw2.summary, `${path}.summary`, errors, 1e3, false);
8388
+ const status = text5(raw2.status, `${path}.status`, errors) ?? "";
8091
8389
  if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
8092
8390
  const revision = integer3(raw2.revision, `${path}.revision`, errors);
8093
8391
  const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
8094
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text4(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
8392
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text5(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
8095
8393
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
8096
8394
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
8097
8395
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
8098
- const bodyMarkdown = text4(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
8099
- const url = text4(raw2.url, `${path}.url`, errors, 2048, false);
8396
+ const bodyMarkdown = text5(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN3, false);
8397
+ const url = text5(raw2.url, `${path}.url`, errors, 2048, false);
8100
8398
  if (url) {
8101
8399
  try {
8102
8400
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -8108,9 +8406,9 @@ function resourceValue(value, path, errors) {
8108
8406
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
8109
8407
  if (asset) unknownKeys3(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
8110
8408
  const brandAsset = asset ? {
8111
- bookId: text4(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
8112
- assetId: text4(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
8113
- ...asset.filename === void 0 ? {} : { filename: text4(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
8409
+ bookId: text5(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
8410
+ assetId: text5(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
8411
+ ...asset.filename === void 0 ? {} : { filename: text5(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
8114
8412
  } : void 0;
8115
8413
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
8116
8414
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -8125,17 +8423,17 @@ function resourceValue(value, path, errors) {
8125
8423
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
8126
8424
  if (promotedRaw) unknownKeys3(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
8127
8425
  const promotedFrom = promotedRaw ? {
8128
- chapterId: text4(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
8129
- resourceId: text4(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
8426
+ chapterId: text5(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
8427
+ resourceId: text5(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
8130
8428
  resourceRevision: integer3(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
8131
8429
  documentRevision: integer3(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
8132
- promotedBy: text4(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
8430
+ promotedBy: text5(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
8133
8431
  promotedAt: integer3(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
8134
8432
  } : void 0;
8135
8433
  const provenance = {
8136
- createdBy: text4(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
8434
+ createdBy: text5(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
8137
8435
  createdAt: integer3(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
8138
- updatedBy: text4(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
8436
+ updatedBy: text5(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
8139
8437
  updatedAt: integer3(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
8140
8438
  ...promotedFrom ? { promotedFrom } : {}
8141
8439
  };
@@ -8154,7 +8452,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
8154
8452
  if (sourceRaw) unknownKeys3(sourceRaw, SOURCE_KEYS2, "document.source", errors);
8155
8453
  const sourceType = sourceRaw?.type;
8156
8454
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
8157
- const sourceAppId = text4(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
8455
+ const sourceAppId = text5(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
8158
8456
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
8159
8457
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
8160
8458
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -8162,8 +8460,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
8162
8460
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
8163
8461
  if (targetRaw) unknownKeys3(targetRaw, TARGET_KEYS2, "document.target", errors);
8164
8462
  const target = targetRaw ? {
8165
- appId: text4(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
8166
- environment: text4(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
8463
+ appId: text5(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
8464
+ environment: text5(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
8167
8465
  } : void 0;
8168
8466
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
8169
8467
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -8301,7 +8599,7 @@ async function verifySource(req, db, ctx) {
8301
8599
  if (ctx.chapter.mode === "chapter") {
8302
8600
  const secret = await getVaultSecret(db, "network_share_secret");
8303
8601
  if (!secret) return null;
8304
- const verified = await (0, import_db12.verifyFederatedRequest)(req, {
8602
+ const verified = await (0, import_db13.verifyFederatedRequest)(req, {
8305
8603
  secret,
8306
8604
  senders: sources.map((source2) => source2.sourceId)
8307
8605
  });
@@ -8314,7 +8612,7 @@ async function verifySource(req, db, ctx) {
8314
8612
  if (!target) continue;
8315
8613
  const secret = await getVaultSecret(db, target.secretName);
8316
8614
  if (!secret) continue;
8317
- const verified = await (0, import_db12.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
8615
+ const verified = await (0, import_db13.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
8318
8616
  if (verified.ok) return { source, body: verified.body };
8319
8617
  }
8320
8618
  return null;
@@ -8504,11 +8802,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
8504
8802
  };
8505
8803
 
8506
8804
  // src/signup-control-delivery.ts
8507
- var import_db13 = require("@odla-ai/db");
8805
+ var import_db14 = require("@odla-ai/db");
8508
8806
  async function deliverSignupControl(options) {
8509
8807
  const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
8510
8808
  const body = JSON.stringify(options.envelope);
8511
- const signed = await (0, import_db13.signFederatedRequest)({
8809
+ const signed = await (0, import_db14.signFederatedRequest)({
8512
8810
  secret: options.secret,
8513
8811
  sender: options.sender,
8514
8812
  method: "POST",
@@ -8529,11 +8827,11 @@ async function deliverSignupControl(options) {
8529
8827
  }
8530
8828
 
8531
8829
  // src/member-content-delivery.ts
8532
- var import_db14 = require("@odla-ai/db");
8830
+ var import_db15 = require("@odla-ai/db");
8533
8831
  async function deliverMemberContent(options) {
8534
8832
  const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
8535
8833
  const body = JSON.stringify(options.envelope);
8536
- const signed = await (0, import_db14.signFederatedRequest)({
8834
+ const signed = await (0, import_db15.signFederatedRequest)({
8537
8835
  secret: options.secret,
8538
8836
  sender: options.sender,
8539
8837
  method: "POST",
@@ -8554,11 +8852,11 @@ async function deliverMemberContent(options) {
8554
8852
  }
8555
8853
 
8556
8854
  // src/builder-library-delivery.ts
8557
- var import_db15 = require("@odla-ai/db");
8855
+ var import_db16 = require("@odla-ai/db");
8558
8856
  async function deliverBuilderLibrary(options) {
8559
8857
  const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
8560
8858
  const body = JSON.stringify(options.envelope);
8561
- const signed = await (0, import_db15.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
8859
+ const signed = await (0, import_db16.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
8562
8860
  const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
8563
8861
  const result = await response.json();
8564
8862
  if (!response.ok || !("ok" in result) || result.ok !== true) {
@@ -8572,7 +8870,7 @@ async function deliverBuilderLibrary(options) {
8572
8870
  var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
8573
8871
  var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
8574
8872
  var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
8575
- var DIGEST = /^sha256:[0-9a-f]{64}$/;
8873
+ var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
8576
8874
  var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
8577
8875
  var MAX_TOKENS = 256;
8578
8876
  var MAX_VALUE_LENGTH = 1e3;
@@ -8616,7 +8914,7 @@ function stateFor(revision, options) {
8616
8914
  metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
8617
8915
  };
8618
8916
  }
8619
- if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
8917
+ if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
8620
8918
  throw new TypeError("signup-control theme revision metadata is invalid");
8621
8919
  }
8622
8920
  return {
@@ -8713,6 +9011,7 @@ var BUILTIN_ROUTES = [
8713
9011
  handleNetworkSnapshot,
8714
9012
  handleNetworkRecords,
8715
9013
  handleNetworkSharedNotes,
9014
+ handleNetworkCommercialParity,
8716
9015
  handleNetworkCommercialStripeWebhook,
8717
9016
  handleFormation,
8718
9017
  handleMember,