@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.
package/dist/index.js CHANGED
@@ -1468,8 +1468,8 @@ var APPLICATION_LABELS = {
1468
1468
  state: "State"
1469
1469
  };
1470
1470
  function humanize(id6) {
1471
- const text5 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1472
- return text5 ? `${text5[0].toUpperCase()}${text5.slice(1)}` : id6;
1471
+ const text6 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1472
+ return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id6;
1473
1473
  }
1474
1474
  function applicationDefs(chapter) {
1475
1475
  try {
@@ -1727,7 +1727,8 @@ function resolveNetwork(config, crm) {
1727
1727
  sharedNotes,
1728
1728
  editableFields,
1729
1729
  stageTransitions,
1730
- ...target.admissionGrants === true ? { admissionGrants: true } : {}
1730
+ ...target.admissionGrants === true ? { admissionGrants: true } : {},
1731
+ ...target.commercialParity === true ? { commercialParity: true } : {}
1731
1732
  });
1732
1733
  }
1733
1734
  const readerIds = /* @__PURE__ */ new Set();
@@ -1742,9 +1743,9 @@ function resolveNetwork(config, crm) {
1742
1743
  readerIds.add(reader.id);
1743
1744
  const readerFields = reader.fields ?? {};
1744
1745
  const entries = Object.entries(readerFields);
1745
- if (entries.length === 0 && !reader.admissionGrants) {
1746
+ if (entries.length === 0 && !reader.admissionGrants && !reader.commercialParity) {
1746
1747
  throw new Error(
1747
- `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only reader`
1748
+ `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only or commercial-parity reader`
1748
1749
  );
1749
1750
  }
1750
1751
  for (const [type, names] of entries) {
@@ -1802,7 +1803,8 @@ function resolveNetwork(config, crm) {
1802
1803
  sharedNotes,
1803
1804
  editableFields,
1804
1805
  stageTransitions,
1805
- ...reader.admissionGrants === true ? { admissionGrants: true } : {}
1806
+ ...reader.admissionGrants === true ? { admissionGrants: true } : {},
1807
+ ...reader.commercialParity === true ? { commercialParity: true } : {}
1806
1808
  });
1807
1809
  }
1808
1810
  return { targets, readers };
@@ -2369,6 +2371,149 @@ function reconcileStripeSnapshot(shadow, snapshots, sources) {
2369
2371
  };
2370
2372
  }
2371
2373
 
2374
+ // src/network-commercial-parity.ts
2375
+ var DIGEST = /^[0-9a-f]{64}$/;
2376
+ var MAX_RECORDS = 500;
2377
+ var text2 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
2378
+ function parityMessage(partition, kind, value) {
2379
+ return [
2380
+ "chapter-commercial-parity",
2381
+ "v1",
2382
+ partition.appId,
2383
+ partition.chapterId,
2384
+ partition.environment,
2385
+ partition.dataEnvironment,
2386
+ partition.runtime,
2387
+ kind,
2388
+ value
2389
+ ].join("");
2390
+ }
2391
+ async function parityKey(secret) {
2392
+ const encoder4 = new TextEncoder();
2393
+ return crypto.subtle.importKey(
2394
+ "raw",
2395
+ encoder4.encode(secret),
2396
+ { name: "HMAC", hash: "SHA-256" },
2397
+ false,
2398
+ ["sign"]
2399
+ );
2400
+ }
2401
+ async function digestWithKey(key, partition, kind, value) {
2402
+ const encoder4 = new TextEncoder();
2403
+ const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
2404
+ return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2405
+ }
2406
+ async function commercialParityDigest(secret, partition, kind, value) {
2407
+ return digestWithKey(await parityKey(secret), partition, kind, value);
2408
+ }
2409
+ async function buildChapterCommercialParitySnapshot(input) {
2410
+ const requestedMax = input.maxRecords ?? MAX_RECORDS;
2411
+ const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
2412
+ const eligible = input.rows.filter((row) => text2(row.id) && (text2(row.stripeCustomerId) || text2(row.stripeSubscriptionId)));
2413
+ const selected2 = eligible.slice(0, maxRecords);
2414
+ const key = await parityKey(input.secret);
2415
+ const records = await Promise.all(selected2.map(async (row) => {
2416
+ const applicationId = text2(row.id);
2417
+ const customerId = text2(row.stripeCustomerId);
2418
+ const subscriptionId = text2(row.stripeSubscriptionId);
2419
+ return {
2420
+ applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
2421
+ customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
2422
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
2423
+ status: text2(row.status, 64),
2424
+ canceled: row.canceled === true
2425
+ };
2426
+ }));
2427
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
2428
+ const truncated = input.scanTruncated || eligible.length > maxRecords;
2429
+ return {
2430
+ version: 1,
2431
+ site: { id: input.siteId },
2432
+ partition: input.partition,
2433
+ generatedAt: input.now ?? Date.now(),
2434
+ complete: !truncated,
2435
+ truncated,
2436
+ counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
2437
+ records
2438
+ };
2439
+ }
2440
+ function recordKey(record4) {
2441
+ return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
2442
+ }
2443
+ function validSnapshot(snapshot, source) {
2444
+ const partition = snapshot?.partition;
2445
+ const records = snapshot?.records;
2446
+ 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;
2447
+ const seen = /* @__PURE__ */ new Set();
2448
+ for (const record4 of records) {
2449
+ if (!DIGEST.test(record4.applicationDigest) || record4.customerDigest !== null && !DIGEST.test(record4.customerDigest) || record4.subscriptionDigest !== null && !DIGEST.test(record4.subscriptionDigest) || record4.status !== null && text2(record4.status, 64) === null || typeof record4.canceled !== "boolean") return false;
2450
+ const key = recordKey(record4);
2451
+ if (seen.has(key)) return false;
2452
+ seen.add(key);
2453
+ }
2454
+ 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;
2455
+ }
2456
+ async function reconcileCommercialParitySnapshot(input) {
2457
+ if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
2458
+ const sourceKey = networkCommercialSourceKey(input.source);
2459
+ const key = await parityKey(input.secret);
2460
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text2(row.applicationId) && text2(row.objectId)).map(async (row) => {
2461
+ const customerId = text2(row.customerId);
2462
+ const subscriptionId = text2(row.subscriptionId) ?? text2(row.objectId);
2463
+ return {
2464
+ applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
2465
+ customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
2466
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
2467
+ status: null,
2468
+ canceled: false,
2469
+ stripeStatus: text2(row.status, 64),
2470
+ stripeCanceled: row.status === "canceled"
2471
+ };
2472
+ }));
2473
+ const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
2474
+ const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
2475
+ const exceptions2 = [];
2476
+ const records = [];
2477
+ for (const [key2, chapter] of chapterByKey) {
2478
+ const stripe = shadowByKey.get(key2);
2479
+ if (!stripe) {
2480
+ exceptions2.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
2481
+ continue;
2482
+ }
2483
+ const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
2484
+ records.push(compared);
2485
+ if (chapter.canceled !== stripe.stripeCanceled) {
2486
+ exceptions2.push({
2487
+ kind: "cancellation_mismatch",
2488
+ applicationDigest: chapter.applicationDigest,
2489
+ detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
2490
+ });
2491
+ }
2492
+ }
2493
+ for (const [key2, stripe] of shadowByKey) {
2494
+ if (!chapterByKey.has(key2)) {
2495
+ exceptions2.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
2496
+ }
2497
+ }
2498
+ if (!input.snapshot.complete || input.snapshot.truncated) {
2499
+ exceptions2.push({
2500
+ kind: "chapter_truncated",
2501
+ detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
2502
+ });
2503
+ }
2504
+ if (chapterByKey.size !== shadowByKey.size) {
2505
+ exceptions2.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
2506
+ }
2507
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
2508
+ return {
2509
+ ok: input.snapshot.complete && exceptions2.length === 0,
2510
+ complete: input.snapshot.complete,
2511
+ counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
2512
+ records,
2513
+ exceptions: exceptions2
2514
+ };
2515
+ }
2516
+
2372
2517
  // src/leader.ts
2373
2518
  import { composeCrmConfigs } from "@odla-ai/crm";
2374
2519
  function leaderCrmConfig(options = {}) {
@@ -2658,6 +2803,7 @@ function createChapterIntegration(chapter, options = {}) {
2658
2803
  path: "/api/network/shared-notes",
2659
2804
  expectedStatus: 401
2660
2805
  }] : [],
2806
+ ...chapter.network.readers.some((reader) => reader.commercialParity) ? [{ path: "/api/network/commercial-parity", expectedStatus: 401 }] : [],
2661
2807
  // Formation metadata is public only when the host explicitly enables
2662
2808
  // the bounded intake contract.
2663
2809
  ...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
@@ -3017,7 +3163,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
3017
3163
  function unknownKeys2(value, allowed2, path, errors) {
3018
3164
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
3019
3165
  }
3020
- function text2(value, path, errors, max = MAX_TEXT2, required = true) {
3166
+ function text3(value, path, errors, max = MAX_TEXT2, required = true) {
3021
3167
  if (value === void 0 && !required) return void 0;
3022
3168
  if (typeof value !== "string" || required && !value.trim()) {
3023
3169
  errors.push(`${path} must be a non-empty string`);
@@ -3043,7 +3189,7 @@ function scopeValue(value, path, errors) {
3043
3189
  return { kind: raw2.kind };
3044
3190
  }
3045
3191
  if (raw2.kind === "chapter") {
3046
- const chapterId = text2(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
3192
+ const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
3047
3193
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
3048
3194
  return { kind: "chapter", chapterId };
3049
3195
  }
@@ -3057,22 +3203,22 @@ function resourceValue(value, path, errors) {
3057
3203
  return null;
3058
3204
  }
3059
3205
  unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
3060
- const id6 = text2(raw2.id, `${path}.id`, errors, 80) ?? "";
3206
+ const id6 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
3061
3207
  if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
3062
- const kind = text2(raw2.kind, `${path}.kind`, errors) ?? "";
3208
+ const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
3063
3209
  if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
3064
- const title = text2(raw2.title, `${path}.title`, errors, 240) ?? "";
3065
- const summary = text2(raw2.summary, `${path}.summary`, errors, 1e3, false);
3066
- const status = text2(raw2.status, `${path}.status`, errors) ?? "";
3210
+ const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
3211
+ const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
3212
+ const status = text3(raw2.status, `${path}.status`, errors) ?? "";
3067
3213
  if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
3068
3214
  const revision = integer2(raw2.revision, `${path}.revision`, errors);
3069
3215
  const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
3070
- const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text2(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
3216
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
3071
3217
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
3072
3218
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
3073
3219
  if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
3074
- const bodyMarkdown = text2(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
3075
- const url = text2(raw2.url, `${path}.url`, errors, 2048, false);
3220
+ const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
3221
+ const url = text3(raw2.url, `${path}.url`, errors, 2048, false);
3076
3222
  if (url) {
3077
3223
  try {
3078
3224
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -3084,9 +3230,9 @@ function resourceValue(value, path, errors) {
3084
3230
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
3085
3231
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
3086
3232
  const brandAsset = asset ? {
3087
- bookId: text2(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
3088
- assetId: text2(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
3089
- ...asset.filename === void 0 ? {} : { filename: text2(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
3233
+ bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
3234
+ assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
3235
+ ...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
3090
3236
  } : void 0;
3091
3237
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
3092
3238
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -3101,17 +3247,17 @@ function resourceValue(value, path, errors) {
3101
3247
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
3102
3248
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
3103
3249
  const promotedFrom = promotedRaw ? {
3104
- chapterId: text2(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
3105
- resourceId: text2(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
3250
+ chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
3251
+ resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
3106
3252
  resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
3107
3253
  documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
3108
- promotedBy: text2(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
3254
+ promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
3109
3255
  promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
3110
3256
  } : void 0;
3111
3257
  const provenance = {
3112
- createdBy: text2(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
3258
+ createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
3113
3259
  createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
3114
- updatedBy: text2(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
3260
+ updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
3115
3261
  updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
3116
3262
  ...promotedFrom ? { promotedFrom } : {}
3117
3263
  };
@@ -3130,7 +3276,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
3130
3276
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
3131
3277
  const sourceType = sourceRaw?.type;
3132
3278
  if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
3133
- const sourceAppId = text2(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
3279
+ const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
3134
3280
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
3135
3281
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
3136
3282
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -3138,8 +3284,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
3138
3284
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
3139
3285
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
3140
3286
  const target = targetRaw ? {
3141
- appId: text2(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
3142
- environment: text2(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
3287
+ appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
3288
+ environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
3143
3289
  } : void 0;
3144
3290
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
3145
3291
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -3263,10 +3409,10 @@ function planDelivery(input) {
3263
3409
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
3264
3410
  const to = redirect ? input.group.debugEmail : input.to;
3265
3411
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
3266
- const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
3412
+ const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
3267
3413
 
3268
3414
  ` + render(tpl.text, vars) : render(tpl.text, vars);
3269
- return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
3415
+ return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
3270
3416
  }
3271
3417
 
3272
3418
  // src/notify.ts
@@ -3672,7 +3818,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
3672
3818
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
3673
3819
  var CURRENCY = /^[a-z]{3}$/;
3674
3820
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
3675
- var text3 = (value, path, errors, max) => {
3821
+ var text4 = (value, path, errors, max) => {
3676
3822
  if (typeof value !== "string" || !value.trim()) {
3677
3823
  errors.push(`${path} is required`);
3678
3824
  return "";
@@ -3700,23 +3846,23 @@ function validateGiftOfferCatalog(input, tierIds) {
3700
3846
  }
3701
3847
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
3702
3848
  for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
3703
- const id6 = text3(item.id, `${path}.id`, errors, 80);
3849
+ const id6 = text4(item.id, `${path}.id`, errors, 80);
3704
3850
  if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3705
3851
  if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
3706
3852
  ids.add(id6);
3707
3853
  const targetRaw = record3(item.target);
3708
3854
  let target = { type: "entitlement", entitlementKey: "invalid" };
3709
3855
  if (targetRaw?.type === "tier") {
3710
- const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3856
+ const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3711
3857
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
3712
3858
  target = { type: "tier", tierId };
3713
3859
  } else if (targetRaw?.type === "entitlement") {
3714
- const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3860
+ const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3715
3861
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
3716
3862
  target = { type: "entitlement", entitlementKey };
3717
3863
  } else errors.push(`${path}.target.type must be tier or entitlement`);
3718
3864
  if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
3719
- const stripePriceId = text3(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
3865
+ const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
3720
3866
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
3721
3867
  if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
3722
3868
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -3738,9 +3884,9 @@ function validateGiftOfferCatalog(input, tierIds) {
3738
3884
  redemptionLifetimeHours: lifetime,
3739
3885
  ...activeFrom !== void 0 ? { activeFrom } : {},
3740
3886
  ...activeUntil !== void 0 ? { activeUntil } : {},
3741
- title: text3(item.title, `${path}.title`, errors, 160),
3742
- description: text3(item.description, `${path}.description`, errors, 2e3),
3743
- ctaLabel: text3(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
3887
+ title: text4(item.title, `${path}.title`, errors, 160),
3888
+ description: text4(item.description, `${path}.description`, errors, 2e3),
3889
+ ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
3744
3890
  });
3745
3891
  }
3746
3892
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -4667,10 +4813,10 @@ function boundedString(value, path, errors, max, required = false) {
4667
4813
  errors.push(`${path} must be a string`);
4668
4814
  return void 0;
4669
4815
  }
4670
- const text5 = value.trim();
4671
- if (required && !text5) errors.push(`${path} is required`);
4672
- if (text5.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
4673
- return text5;
4816
+ const text6 = value.trim();
4817
+ if (required && !text6) errors.push(`${path} is required`);
4818
+ if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
4819
+ return text6;
4674
4820
  }
4675
4821
  function validateMembershipPageDocument(input, options = {}) {
4676
4822
  const errors = [];
@@ -4719,8 +4865,8 @@ function validateMembershipPageDocument(input, options = {}) {
4719
4865
  else {
4720
4866
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
4721
4867
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
4722
- const text5 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
4723
- return text5 ? [text5] : [];
4868
+ const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
4869
+ return text6 ? [text6] : [];
4724
4870
  });
4725
4871
  }
4726
4872
  }
@@ -4819,7 +4965,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
4819
4965
  function unknownKeys3(value, allowed2, path, errors) {
4820
4966
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
4821
4967
  }
4822
- function text4(value, path, errors, max = 65536) {
4968
+ function text5(value, path, errors, max = 65536) {
4823
4969
  if (typeof value !== "string" || !value.trim()) {
4824
4970
  errors.push(`${path} is required`);
4825
4971
  return "";
@@ -4842,11 +4988,11 @@ function tiersOf(value, errors) {
4842
4988
  return [];
4843
4989
  }
4844
4990
  unknownKeys3(item, TIER_KEYS2, path, errors);
4845
- const id6 = text4(item.id, `${path}.id`, errors, 80);
4991
+ const id6 = text5(item.id, `${path}.id`, errors, 80);
4846
4992
  if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
4847
4993
  if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
4848
4994
  seen.add(id6);
4849
- const name = text4(item.name, `${path}.name`, errors, 160);
4995
+ const name = text5(item.name, `${path}.name`, errors, 160);
4850
4996
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
4851
4997
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
4852
4998
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -4858,7 +5004,7 @@ function tiersOf(value, errors) {
4858
5004
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
4859
5005
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
4860
5006
  }
4861
- const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
5007
+ const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
4862
5008
  return [{
4863
5009
  id: id6,
4864
5010
  name,
@@ -4914,7 +5060,7 @@ function policyOf(value, errors) {
4914
5060
  const result = {};
4915
5061
  for (const key of allowed2) {
4916
5062
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
4917
- result[key] = text4(input[key], `document.policy.${key}`, errors);
5063
+ result[key] = text5(input[key], `document.policy.${key}`, errors);
4918
5064
  }
4919
5065
  return result;
4920
5066
  }
@@ -4952,9 +5098,9 @@ function validateSignupControlDocument(input) {
4952
5098
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
4953
5099
  const source = recordOf(root.source);
4954
5100
  const target = recordOf(root.target);
4955
- const appId = text4(source?.appId, "document.source.appId", errors, 63);
4956
- const chapterId = text4(target?.chapterId, "document.target.chapterId", errors, 63);
4957
- const environment = text4(target?.environment, "document.target.environment", errors, 63);
5101
+ const appId = text5(source?.appId, "document.source.appId", errors, 63);
5102
+ const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
5103
+ const environment = text5(target?.environment, "document.target.environment", errors, 63);
4958
5104
  if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
4959
5105
  if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
4960
5106
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -4998,7 +5144,7 @@ function validateSignupControlDocument(input) {
4998
5144
  var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
4999
5145
  var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
5000
5146
  var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
5001
- var DIGEST = /^sha256:[0-9a-f]{64}$/;
5147
+ var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
5002
5148
  var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
5003
5149
  var MAX_TOKENS = 256;
5004
5150
  var MAX_VALUE_LENGTH = 1e3;
@@ -5042,7 +5188,7 @@ function stateFor(revision, options) {
5042
5188
  metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
5043
5189
  };
5044
5190
  }
5045
- if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
5191
+ if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
5046
5192
  throw new TypeError("signup-control theme revision metadata is invalid");
5047
5193
  }
5048
5194
  return {
@@ -5131,9 +5277,9 @@ function markdownBenefits(lines, path, errors) {
5131
5277
  errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
5132
5278
  continue;
5133
5279
  }
5134
- const text5 = (match[1] ?? "").trim();
5135
- if (text5.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
5136
- if (text5) benefits.push(text5);
5280
+ const text6 = (match[1] ?? "").trim();
5281
+ if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
5282
+ if (text6) benefits.push(text6);
5137
5283
  }
5138
5284
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
5139
5285
  return benefits.slice(0, MAX_BENEFITS2);
@@ -5800,6 +5946,7 @@ export {
5800
5946
  brandTokens,
5801
5947
  bucketSeries,
5802
5948
  buildBuilderLibraryManifest,
5949
+ buildChapterCommercialParitySnapshot,
5803
5950
  buildGroupSeed,
5804
5951
  buildTierSeeds,
5805
5952
  builderLibraryDigest,
@@ -5825,6 +5972,7 @@ export {
5825
5972
  clerkListUsers,
5826
5973
  clerkSetRole,
5827
5974
  clerkUserRequest,
5975
+ commercialParityDigest,
5828
5976
  composeMemberContent,
5829
5977
  createAdmissionGrantEnvelope,
5830
5978
  createBuilderLibraryEnvelope,
@@ -5897,6 +6045,7 @@ export {
5897
6045
  projectSharedRecord,
5898
6046
  promoteBuilderLibraryResource,
5899
6047
  publicGiftOffers,
6048
+ reconcileCommercialParitySnapshot,
5900
6049
  reconcileMeetings,
5901
6050
  reconcileStripeSnapshot,
5902
6051
  refundedPatch,