@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/README.md +22 -2
- package/dist/{copy-context-CIxmneHI.d.ts → copy-context-20WFI5TI.d.ts} +17 -4
- package/dist/index.cjs +212 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -10
- package/dist/index.d.ts +104 -10
- package/dist/index.js +212 -58
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +363 -60
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +17 -4
- package/dist/worker/index.d.ts +17 -4
- package/dist/worker/index.js +363 -60
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
|
1472
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3213
|
+
const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
3063
3214
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
3064
|
-
const title =
|
|
3065
|
-
const summary =
|
|
3066
|
-
const status =
|
|
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) =>
|
|
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 =
|
|
3075
|
-
const url =
|
|
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:
|
|
3088
|
-
assetId:
|
|
3089
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
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:
|
|
3105
|
-
resourceId:
|
|
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:
|
|
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:
|
|
3263
|
+
createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
3113
3264
|
createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
3114
|
-
updatedBy:
|
|
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 =
|
|
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:
|
|
3142
|
-
environment:
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
3742
|
-
description:
|
|
3743
|
-
ctaLabel:
|
|
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
|
|
4671
|
-
if (required && !
|
|
4672
|
-
if (
|
|
4673
|
-
return
|
|
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
|
|
4723
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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 :
|
|
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] =
|
|
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 =
|
|
4956
|
-
const chapterId =
|
|
4957
|
-
const environment =
|
|
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
|
|
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 || !
|
|
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
|
|
5135
|
-
if (
|
|
5136
|
-
if (
|
|
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,
|