@odla-ai/chapter 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -801,7 +801,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
801
801
  }
802
802
  }
803
803
  if (auth.source === "table") entities.admins = admins;
804
- if (auth.superAdmins) entities.superAdmins = superAdmins;
804
+ if (auth.superAdminSource === "table") entities.superAdmins = superAdmins;
805
805
  if (includeNetworkNotes) entities.networkNotes = networkNotes;
806
806
  if (includeBuilderLibrary) {
807
807
  entities.builderLibraryControls = builderLibraryControls;
@@ -984,7 +984,12 @@ function resolveAuth(mode, auth) {
984
984
  }
985
985
  const adminRole = ladder[ladder.length - 1];
986
986
  const superAdmins2 = a.superAdmins ?? source === "claim";
987
- return { source, claim, ladder, adminRole, superAdmins: superAdmins2 };
987
+ const configuredSuperSource = a.superAdminSource ?? "table";
988
+ if (configuredSuperSource !== "table" && configuredSuperSource !== "odla") {
989
+ throw new Error(`defineChapter.auth.superAdminSource: must be "table" or "odla" \u2014 got ${JSON.stringify(a.superAdminSource)}`);
990
+ }
991
+ const superAdminSource = superAdmins2 ? configuredSuperSource : "none";
992
+ return { source, claim, ladder, adminRole, superAdmins: superAdmins2, superAdminSource };
988
993
  }
989
994
  function roleFromClaim(payload, auth) {
990
995
  const raw2 = payload[auth.claim];
@@ -1468,8 +1473,8 @@ var APPLICATION_LABELS = {
1468
1473
  state: "State"
1469
1474
  };
1470
1475
  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;
1476
+ const text6 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1477
+ return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id6;
1473
1478
  }
1474
1479
  function applicationDefs(chapter) {
1475
1480
  try {
@@ -1727,7 +1732,8 @@ function resolveNetwork(config, crm) {
1727
1732
  sharedNotes,
1728
1733
  editableFields,
1729
1734
  stageTransitions,
1730
- ...target.admissionGrants === true ? { admissionGrants: true } : {}
1735
+ ...target.admissionGrants === true ? { admissionGrants: true } : {},
1736
+ ...target.commercialParity === true ? { commercialParity: true } : {}
1731
1737
  });
1732
1738
  }
1733
1739
  const readerIds = /* @__PURE__ */ new Set();
@@ -1742,9 +1748,9 @@ function resolveNetwork(config, crm) {
1742
1748
  readerIds.add(reader.id);
1743
1749
  const readerFields = reader.fields ?? {};
1744
1750
  const entries = Object.entries(readerFields);
1745
- if (entries.length === 0 && !reader.admissionGrants) {
1751
+ if (entries.length === 0 && !reader.admissionGrants && !reader.commercialParity) {
1746
1752
  throw new Error(
1747
- `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only reader`
1753
+ `defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only or commercial-parity reader`
1748
1754
  );
1749
1755
  }
1750
1756
  for (const [type, names] of entries) {
@@ -1802,7 +1808,8 @@ function resolveNetwork(config, crm) {
1802
1808
  sharedNotes,
1803
1809
  editableFields,
1804
1810
  stageTransitions,
1805
- ...reader.admissionGrants === true ? { admissionGrants: true } : {}
1811
+ ...reader.admissionGrants === true ? { admissionGrants: true } : {},
1812
+ ...reader.commercialParity === true ? { commercialParity: true } : {}
1806
1813
  });
1807
1814
  }
1808
1815
  return { targets, readers };
@@ -2369,6 +2376,149 @@ function reconcileStripeSnapshot(shadow, snapshots, sources) {
2369
2376
  };
2370
2377
  }
2371
2378
 
2379
+ // src/network-commercial-parity.ts
2380
+ var DIGEST = /^[0-9a-f]{64}$/;
2381
+ var MAX_RECORDS = 500;
2382
+ var text2 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
2383
+ function parityMessage(partition, kind, value) {
2384
+ return [
2385
+ "chapter-commercial-parity",
2386
+ "v1",
2387
+ partition.appId,
2388
+ partition.chapterId,
2389
+ partition.environment,
2390
+ partition.dataEnvironment,
2391
+ partition.runtime,
2392
+ kind,
2393
+ value
2394
+ ].join("");
2395
+ }
2396
+ async function parityKey(secret) {
2397
+ const encoder4 = new TextEncoder();
2398
+ return crypto.subtle.importKey(
2399
+ "raw",
2400
+ encoder4.encode(secret),
2401
+ { name: "HMAC", hash: "SHA-256" },
2402
+ false,
2403
+ ["sign"]
2404
+ );
2405
+ }
2406
+ async function digestWithKey(key, partition, kind, value) {
2407
+ const encoder4 = new TextEncoder();
2408
+ const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
2409
+ return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
2410
+ }
2411
+ async function commercialParityDigest(secret, partition, kind, value) {
2412
+ return digestWithKey(await parityKey(secret), partition, kind, value);
2413
+ }
2414
+ async function buildChapterCommercialParitySnapshot(input) {
2415
+ const requestedMax = input.maxRecords ?? MAX_RECORDS;
2416
+ const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
2417
+ const eligible = input.rows.filter((row) => text2(row.id) && (text2(row.stripeCustomerId) || text2(row.stripeSubscriptionId)));
2418
+ const selected2 = eligible.slice(0, maxRecords);
2419
+ const key = await parityKey(input.secret);
2420
+ const records = await Promise.all(selected2.map(async (row) => {
2421
+ const applicationId = text2(row.id);
2422
+ const customerId = text2(row.stripeCustomerId);
2423
+ const subscriptionId = text2(row.stripeSubscriptionId);
2424
+ return {
2425
+ applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
2426
+ customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
2427
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
2428
+ status: text2(row.status, 64),
2429
+ canceled: row.canceled === true
2430
+ };
2431
+ }));
2432
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
2433
+ const truncated = input.scanTruncated || eligible.length > maxRecords;
2434
+ return {
2435
+ version: 1,
2436
+ site: { id: input.siteId },
2437
+ partition: input.partition,
2438
+ generatedAt: input.now ?? Date.now(),
2439
+ complete: !truncated,
2440
+ truncated,
2441
+ counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
2442
+ records
2443
+ };
2444
+ }
2445
+ function recordKey(record4) {
2446
+ return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
2447
+ }
2448
+ function validSnapshot(snapshot, source) {
2449
+ const partition = snapshot?.partition;
2450
+ const records = snapshot?.records;
2451
+ 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;
2452
+ const seen = /* @__PURE__ */ new Set();
2453
+ for (const record4 of records) {
2454
+ 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;
2455
+ const key = recordKey(record4);
2456
+ if (seen.has(key)) return false;
2457
+ seen.add(key);
2458
+ }
2459
+ 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;
2460
+ }
2461
+ async function reconcileCommercialParitySnapshot(input) {
2462
+ if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
2463
+ const sourceKey = networkCommercialSourceKey(input.source);
2464
+ const key = await parityKey(input.secret);
2465
+ const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text2(row.applicationId) && text2(row.objectId)).map(async (row) => {
2466
+ const customerId = text2(row.customerId);
2467
+ const subscriptionId = text2(row.subscriptionId) ?? text2(row.objectId);
2468
+ return {
2469
+ applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
2470
+ customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
2471
+ subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
2472
+ status: null,
2473
+ canceled: false,
2474
+ stripeStatus: text2(row.status, 64),
2475
+ stripeCanceled: row.status === "canceled"
2476
+ };
2477
+ }));
2478
+ const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
2479
+ const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
2480
+ const exceptions2 = [];
2481
+ const records = [];
2482
+ for (const [key2, chapter] of chapterByKey) {
2483
+ const stripe = shadowByKey.get(key2);
2484
+ if (!stripe) {
2485
+ exceptions2.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
2486
+ continue;
2487
+ }
2488
+ const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
2489
+ records.push(compared);
2490
+ if (chapter.canceled !== stripe.stripeCanceled) {
2491
+ exceptions2.push({
2492
+ kind: "cancellation_mismatch",
2493
+ applicationDigest: chapter.applicationDigest,
2494
+ detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
2495
+ });
2496
+ }
2497
+ }
2498
+ for (const [key2, stripe] of shadowByKey) {
2499
+ if (!chapterByKey.has(key2)) {
2500
+ exceptions2.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
2501
+ }
2502
+ }
2503
+ if (!input.snapshot.complete || input.snapshot.truncated) {
2504
+ exceptions2.push({
2505
+ kind: "chapter_truncated",
2506
+ detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
2507
+ });
2508
+ }
2509
+ if (chapterByKey.size !== shadowByKey.size) {
2510
+ exceptions2.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
2511
+ }
2512
+ records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
2513
+ return {
2514
+ ok: input.snapshot.complete && exceptions2.length === 0,
2515
+ complete: input.snapshot.complete,
2516
+ counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
2517
+ records,
2518
+ exceptions: exceptions2
2519
+ };
2520
+ }
2521
+
2372
2522
  // src/leader.ts
2373
2523
  import { composeCrmConfigs } from "@odla-ai/crm";
2374
2524
  function leaderCrmConfig(options = {}) {
@@ -2658,6 +2808,7 @@ function createChapterIntegration(chapter, options = {}) {
2658
2808
  path: "/api/network/shared-notes",
2659
2809
  expectedStatus: 401
2660
2810
  }] : [],
2811
+ ...chapter.network.readers.some((reader) => reader.commercialParity) ? [{ path: "/api/network/commercial-parity", expectedStatus: 401 }] : [],
2661
2812
  // Formation metadata is public only when the host explicitly enables
2662
2813
  // the bounded intake contract.
2663
2814
  ...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
@@ -3017,7 +3168,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
3017
3168
  function unknownKeys2(value, allowed2, path, errors) {
3018
3169
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
3019
3170
  }
3020
- function text2(value, path, errors, max = MAX_TEXT2, required = true) {
3171
+ function text3(value, path, errors, max = MAX_TEXT2, required = true) {
3021
3172
  if (value === void 0 && !required) return void 0;
3022
3173
  if (typeof value !== "string" || required && !value.trim()) {
3023
3174
  errors.push(`${path} must be a non-empty string`);
@@ -3043,7 +3194,7 @@ function scopeValue(value, path, errors) {
3043
3194
  return { kind: raw2.kind };
3044
3195
  }
3045
3196
  if (raw2.kind === "chapter") {
3046
- const chapterId = text2(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
3197
+ const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
3047
3198
  if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
3048
3199
  return { kind: "chapter", chapterId };
3049
3200
  }
@@ -3057,22 +3208,22 @@ function resourceValue(value, path, errors) {
3057
3208
  return null;
3058
3209
  }
3059
3210
  unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
3060
- const id6 = text2(raw2.id, `${path}.id`, errors, 80) ?? "";
3211
+ const id6 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
3061
3212
  if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
3062
- const kind = text2(raw2.kind, `${path}.kind`, errors) ?? "";
3213
+ const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
3063
3214
  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) ?? "";
3215
+ const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
3216
+ const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
3217
+ const status = text3(raw2.status, `${path}.status`, errors) ?? "";
3067
3218
  if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
3068
3219
  const revision = integer2(raw2.revision, `${path}.revision`, errors);
3069
3220
  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) ?? "") : [];
3221
+ const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
3071
3222
  if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
3072
3223
  if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
3073
3224
  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);
3225
+ const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
3226
+ const url = text3(raw2.url, `${path}.url`, errors, 2048, false);
3076
3227
  if (url) {
3077
3228
  try {
3078
3229
  if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
@@ -3084,9 +3235,9 @@ function resourceValue(value, path, errors) {
3084
3235
  if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
3085
3236
  if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
3086
3237
  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) ?? "" }
3238
+ bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
3239
+ assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
3240
+ ...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
3090
3241
  } : void 0;
3091
3242
  if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
3092
3243
  if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
@@ -3101,17 +3252,17 @@ function resourceValue(value, path, errors) {
3101
3252
  if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
3102
3253
  if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
3103
3254
  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) ?? "",
3255
+ chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
3256
+ resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
3106
3257
  resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
3107
3258
  documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
3108
- promotedBy: text2(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
3259
+ promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
3109
3260
  promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
3110
3261
  } : void 0;
3111
3262
  const provenance = {
3112
- createdBy: text2(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
3263
+ createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
3113
3264
  createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
3114
- updatedBy: text2(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
3265
+ updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
3115
3266
  updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
3116
3267
  ...promotedFrom ? { promotedFrom } : {}
3117
3268
  };
@@ -3130,7 +3281,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
3130
3281
  if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
3131
3282
  const sourceType = sourceRaw?.type;
3132
3283
  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) ?? "";
3284
+ const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
3134
3285
  if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
3135
3286
  if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
3136
3287
  if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
@@ -3138,8 +3289,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
3138
3289
  if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
3139
3290
  if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
3140
3291
  const target = targetRaw ? {
3141
- appId: text2(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
3142
- environment: text2(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
3292
+ appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
3293
+ environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
3143
3294
  } : void 0;
3144
3295
  if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
3145
3296
  if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
@@ -3263,10 +3414,10 @@ function planDelivery(input) {
3263
3414
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
3264
3415
  const to = redirect ? input.group.debugEmail : input.to;
3265
3416
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
3266
- const text5 = redirect ? `(dev redirect; original recipient: ${input.to})
3417
+ const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
3267
3418
 
3268
3419
  ` + render(tpl.text, vars) : render(tpl.text, vars);
3269
- return { deliver: true, transport, to, subject, text: text5, redirected: redirect };
3420
+ return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
3270
3421
  }
3271
3422
 
3272
3423
  // src/notify.ts
@@ -3672,7 +3823,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
3672
3823
  var PRICE = /^price_[A-Za-z0-9]{12,}$/;
3673
3824
  var CURRENCY = /^[a-z]{3}$/;
3674
3825
  var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
3675
- var text3 = (value, path, errors, max) => {
3826
+ var text4 = (value, path, errors, max) => {
3676
3827
  if (typeof value !== "string" || !value.trim()) {
3677
3828
  errors.push(`${path} is required`);
3678
3829
  return "";
@@ -3700,23 +3851,23 @@ function validateGiftOfferCatalog(input, tierIds) {
3700
3851
  }
3701
3852
  const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
3702
3853
  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);
3854
+ const id6 = text4(item.id, `${path}.id`, errors, 80);
3704
3855
  if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3705
3856
  if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
3706
3857
  ids.add(id6);
3707
3858
  const targetRaw = record3(item.target);
3708
3859
  let target = { type: "entitlement", entitlementKey: "invalid" };
3709
3860
  if (targetRaw?.type === "tier") {
3710
- const tierId = text3(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3861
+ const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
3711
3862
  if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
3712
3863
  target = { type: "tier", tierId };
3713
3864
  } else if (targetRaw?.type === "entitlement") {
3714
- const entitlementKey = text3(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3865
+ const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
3715
3866
  if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
3716
3867
  target = { type: "entitlement", entitlementKey };
3717
3868
  } else errors.push(`${path}.target.type must be tier or entitlement`);
3718
3869
  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);
3870
+ const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
3720
3871
  if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
3721
3872
  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
3873
  const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
@@ -3738,9 +3889,9 @@ function validateGiftOfferCatalog(input, tierIds) {
3738
3889
  redemptionLifetimeHours: lifetime,
3739
3890
  ...activeFrom !== void 0 ? { activeFrom } : {},
3740
3891
  ...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)
3892
+ title: text4(item.title, `${path}.title`, errors, 160),
3893
+ description: text4(item.description, `${path}.description`, errors, 2e3),
3894
+ ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
3744
3895
  });
3745
3896
  }
3746
3897
  return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
@@ -4667,10 +4818,10 @@ function boundedString(value, path, errors, max, required = false) {
4667
4818
  errors.push(`${path} must be a string`);
4668
4819
  return void 0;
4669
4820
  }
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;
4821
+ const text6 = value.trim();
4822
+ if (required && !text6) errors.push(`${path} is required`);
4823
+ if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
4824
+ return text6;
4674
4825
  }
4675
4826
  function validateMembershipPageDocument(input, options = {}) {
4676
4827
  const errors = [];
@@ -4719,8 +4870,8 @@ function validateMembershipPageDocument(input, options = {}) {
4719
4870
  else {
4720
4871
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
4721
4872
  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] : [];
4873
+ const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
4874
+ return text6 ? [text6] : [];
4724
4875
  });
4725
4876
  }
4726
4877
  }
@@ -4819,7 +4970,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
4819
4970
  function unknownKeys3(value, allowed2, path, errors) {
4820
4971
  for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
4821
4972
  }
4822
- function text4(value, path, errors, max = 65536) {
4973
+ function text5(value, path, errors, max = 65536) {
4823
4974
  if (typeof value !== "string" || !value.trim()) {
4824
4975
  errors.push(`${path} is required`);
4825
4976
  return "";
@@ -4842,11 +4993,11 @@ function tiersOf(value, errors) {
4842
4993
  return [];
4843
4994
  }
4844
4995
  unknownKeys3(item, TIER_KEYS2, path, errors);
4845
- const id6 = text4(item.id, `${path}.id`, errors, 80);
4996
+ const id6 = text5(item.id, `${path}.id`, errors, 80);
4846
4997
  if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
4847
4998
  if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
4848
4999
  seen.add(id6);
4849
- const name = text4(item.name, `${path}.name`, errors, 160);
5000
+ const name = text5(item.name, `${path}.name`, errors, 160);
4850
5001
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
4851
5002
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
4852
5003
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
@@ -4858,7 +5009,7 @@ function tiersOf(value, errors) {
4858
5009
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
4859
5010
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
4860
5011
  }
4861
- const blurb = item.blurb === void 0 ? void 0 : text4(item.blurb, `${path}.blurb`, errors, 4e3);
5012
+ const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
4862
5013
  return [{
4863
5014
  id: id6,
4864
5015
  name,
@@ -4914,7 +5065,7 @@ function policyOf(value, errors) {
4914
5065
  const result = {};
4915
5066
  for (const key of allowed2) {
4916
5067
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
4917
- result[key] = text4(input[key], `document.policy.${key}`, errors);
5068
+ result[key] = text5(input[key], `document.policy.${key}`, errors);
4918
5069
  }
4919
5070
  return result;
4920
5071
  }
@@ -4952,9 +5103,9 @@ function validateSignupControlDocument(input) {
4952
5103
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
4953
5104
  const source = recordOf(root.source);
4954
5105
  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);
5106
+ const appId = text5(source?.appId, "document.source.appId", errors, 63);
5107
+ const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
5108
+ const environment = text5(target?.environment, "document.target.environment", errors, 63);
4958
5109
  if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
4959
5110
  if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
4960
5111
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -4998,7 +5149,7 @@ function validateSignupControlDocument(input) {
4998
5149
  var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
4999
5150
  var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
5000
5151
  var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
5001
- var DIGEST = /^sha256:[0-9a-f]{64}$/;
5152
+ var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
5002
5153
  var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
5003
5154
  var MAX_TOKENS = 256;
5004
5155
  var MAX_VALUE_LENGTH = 1e3;
@@ -5042,7 +5193,7 @@ function stateFor(revision, options) {
5042
5193
  metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
5043
5194
  };
5044
5195
  }
5045
- if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
5196
+ if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
5046
5197
  throw new TypeError("signup-control theme revision metadata is invalid");
5047
5198
  }
5048
5199
  return {
@@ -5131,9 +5282,9 @@ function markdownBenefits(lines, path, errors) {
5131
5282
  errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
5132
5283
  continue;
5133
5284
  }
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);
5285
+ const text6 = (match[1] ?? "").trim();
5286
+ if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
5287
+ if (text6) benefits.push(text6);
5137
5288
  }
5138
5289
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
5139
5290
  return benefits.slice(0, MAX_BENEFITS2);
@@ -5800,6 +5951,7 @@ export {
5800
5951
  brandTokens,
5801
5952
  bucketSeries,
5802
5953
  buildBuilderLibraryManifest,
5954
+ buildChapterCommercialParitySnapshot,
5803
5955
  buildGroupSeed,
5804
5956
  buildTierSeeds,
5805
5957
  builderLibraryDigest,
@@ -5825,6 +5977,7 @@ export {
5825
5977
  clerkListUsers,
5826
5978
  clerkSetRole,
5827
5979
  clerkUserRequest,
5980
+ commercialParityDigest,
5828
5981
  composeMemberContent,
5829
5982
  createAdmissionGrantEnvelope,
5830
5983
  createBuilderLibraryEnvelope,
@@ -5897,6 +6050,7 @@ export {
5897
6050
  projectSharedRecord,
5898
6051
  promoteBuilderLibraryResource,
5899
6052
  publicGiftOffers,
6053
+ reconcileCommercialParitySnapshot,
5900
6054
  reconcileMeetings,
5901
6055
  reconcileStripeSnapshot,
5902
6056
  refundedPatch,