@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/README.md +18 -0
- package/dist/{copy-context-CIxmneHI.d.ts → copy-context-GaxckY3T.d.ts} +9 -0
- package/dist/index.cjs +205 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -1
- package/dist/index.d.ts +88 -1
- package/dist/index.js +205 -56
- 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 +357 -58
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +9 -0
- package/dist/worker/index.d.ts +9 -0
- package/dist/worker/index.js +357 -58
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
1472
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3208
|
+
const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
3063
3209
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
3064
|
-
const title =
|
|
3065
|
-
const summary =
|
|
3066
|
-
const status =
|
|
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) =>
|
|
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 =
|
|
3075
|
-
const url =
|
|
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:
|
|
3088
|
-
assetId:
|
|
3089
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
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:
|
|
3105
|
-
resourceId:
|
|
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:
|
|
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:
|
|
3258
|
+
createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
3113
3259
|
createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
3114
|
-
updatedBy:
|
|
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 =
|
|
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:
|
|
3142
|
-
environment:
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
3742
|
-
description:
|
|
3743
|
-
ctaLabel:
|
|
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
|
|
4671
|
-
if (required && !
|
|
4672
|
-
if (
|
|
4673
|
-
return
|
|
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
|
|
4723
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
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 :
|
|
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] =
|
|
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 =
|
|
4956
|
-
const chapterId =
|
|
4957
|
-
const environment =
|
|
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
|
|
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 || !
|
|
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
|
|
5135
|
-
if (
|
|
5136
|
-
if (
|
|
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,
|