@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/README.md
CHANGED
|
@@ -811,6 +811,8 @@ export const chapter = defineChapter({
|
|
|
811
811
|
name: "Example Chapter",
|
|
812
812
|
url: "https://chapter.example.com",
|
|
813
813
|
fields: { person: ["name", "email"] },
|
|
814
|
+
// Independent consent to compare the chapter's commercial projection.
|
|
815
|
+
commercialParity: true,
|
|
814
816
|
}],
|
|
815
817
|
},
|
|
816
818
|
});
|
|
@@ -900,6 +902,20 @@ removing it changes no customer-facing behavior. The BNF frontend may be down
|
|
|
900
902
|
while the webhook continues to ingest. Registering a Stripe endpoint and
|
|
901
903
|
enabling production remain separate reviewed operations.
|
|
902
904
|
|
|
905
|
+
`POST` with `{ command: "reconcile-chapters" }` is a separate, two-sided opt-in
|
|
906
|
+
comparison. A hub target and the matching follower reader must both set
|
|
907
|
+
`commercialParity: true`; neither record browsing nor Stripe shadow mode grants
|
|
908
|
+
it implicitly. The hub signs a `GET /api/network/commercial-parity` request with
|
|
909
|
+
the edge's `network_share_*` secret. The chapter returns only bounded status,
|
|
910
|
+
cancellation, counts, completeness, and domain-separated HMAC-SHA-256 digests of
|
|
911
|
+
application, Stripe customer, and Stripe subscription identifiers. Name, email,
|
|
912
|
+
raw identifiers, and arbitrary application fields never cross the edge. The hub
|
|
913
|
+
compares those digests with its exact Stripe partition and reports chapter-only,
|
|
914
|
+
shadow-only, cancellation, count, unavailable, invalid, and truncation
|
|
915
|
+
exceptions without persisting or changing either side. Chapter application
|
|
916
|
+
status and Stripe subscription status remain distinct vocabularies and are shown
|
|
917
|
+
side by side rather than falsely compared for equality.
|
|
918
|
+
|
|
903
919
|
For a hub with network targets, the standard admin catalog is Dashboard,
|
|
904
920
|
Network, People, Portfolio, and Settings. Dashboard Overview renders aggregate network
|
|
905
921
|
health; People contains CRM types whose `workspace` is absent or `"people"`;
|
|
@@ -934,6 +950,8 @@ export const chapter = defineChapter({
|
|
|
934
950
|
editableFields: { person: ["name", "email", "phone", "linkedin"] },
|
|
935
951
|
stageTransitions: ["person"],
|
|
936
952
|
admissionGrants: true,
|
|
953
|
+
// Independent disclosure consent. This may be the reader's only ability.
|
|
954
|
+
commercialParity: true,
|
|
937
955
|
}],
|
|
938
956
|
},
|
|
939
957
|
});
|
|
@@ -229,6 +229,10 @@ interface ChapterNetworkTarget {
|
|
|
229
229
|
/** Allow package-owned desired admission decisions for existing leader CRM
|
|
230
230
|
* people to be delivered to this target. Omitted means default deny. */
|
|
231
231
|
admissionGrants?: boolean;
|
|
232
|
+
/** Allow this leader to request a PII-free commercial parity snapshot from
|
|
233
|
+
* this follower. The follower must independently enable the same reader;
|
|
234
|
+
* omitted means no application or Stripe identifier digest crosses. */
|
|
235
|
+
commercialParity?: boolean;
|
|
232
236
|
}
|
|
233
237
|
/** One signed peer allowed to browse a privacy-bounded projection of this
|
|
234
238
|
* site's CRM. The follower owns this allowlist; a leader target declaration
|
|
@@ -253,6 +257,9 @@ interface ChapterNetworkReader {
|
|
|
253
257
|
/** Accept signed desired admission decisions from this reader. Independent
|
|
254
258
|
* of record browsing; an admission-only reader may omit `fields`. */
|
|
255
259
|
admissionGrants?: boolean;
|
|
260
|
+
/** Disclose a bounded, PII-free commercial parity snapshot to this signed
|
|
261
|
+
* reader. Identifiers leave only as edge-keyed, domain-separated HMACs. */
|
|
262
|
+
commercialParity?: boolean;
|
|
256
263
|
}
|
|
257
264
|
/** Leader/follower network configuration. A follower needs no code config to
|
|
258
265
|
* receive pushed records. It must explicitly declare `readers` before any peer
|
|
@@ -298,6 +305,7 @@ interface ResolvedNetworkTarget {
|
|
|
298
305
|
editableFields: Record<string, readonly string[]>;
|
|
299
306
|
stageTransitions: readonly string[];
|
|
300
307
|
admissionGrants?: boolean;
|
|
308
|
+
commercialParity?: boolean;
|
|
301
309
|
}
|
|
302
310
|
/** A validated follower-owned record projection for one signed reader. */
|
|
303
311
|
interface ResolvedNetworkReader {
|
|
@@ -307,6 +315,7 @@ interface ResolvedNetworkReader {
|
|
|
307
315
|
editableFields: Record<string, readonly string[]>;
|
|
308
316
|
stageTransitions: readonly string[];
|
|
309
317
|
admissionGrants?: boolean;
|
|
318
|
+
commercialParity?: boolean;
|
|
310
319
|
}
|
|
311
320
|
/** Fully-resolved leader network configuration. */
|
|
312
321
|
interface ResolvedNetwork {
|
package/dist/index.cjs
CHANGED
|
@@ -61,6 +61,7 @@ __export(index_exports, {
|
|
|
61
61
|
brandTokens: () => brandTokens,
|
|
62
62
|
bucketSeries: () => bucketSeries,
|
|
63
63
|
buildBuilderLibraryManifest: () => buildBuilderLibraryManifest,
|
|
64
|
+
buildChapterCommercialParitySnapshot: () => buildChapterCommercialParitySnapshot,
|
|
64
65
|
buildGroupSeed: () => buildGroupSeed,
|
|
65
66
|
buildTierSeeds: () => buildTierSeeds,
|
|
66
67
|
builderLibraryDigest: () => builderLibraryDigest,
|
|
@@ -86,6 +87,7 @@ __export(index_exports, {
|
|
|
86
87
|
clerkListUsers: () => clerkListUsers,
|
|
87
88
|
clerkSetRole: () => clerkSetRole,
|
|
88
89
|
clerkUserRequest: () => clerkUserRequest,
|
|
90
|
+
commercialParityDigest: () => commercialParityDigest,
|
|
89
91
|
composeMemberContent: () => composeMemberContent,
|
|
90
92
|
createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
|
|
91
93
|
createBuilderLibraryEnvelope: () => createBuilderLibraryEnvelope,
|
|
@@ -158,6 +160,7 @@ __export(index_exports, {
|
|
|
158
160
|
projectSharedRecord: () => projectSharedRecord,
|
|
159
161
|
promoteBuilderLibraryResource: () => promoteBuilderLibraryResource,
|
|
160
162
|
publicGiftOffers: () => publicGiftOffers,
|
|
163
|
+
reconcileCommercialParitySnapshot: () => reconcileCommercialParitySnapshot,
|
|
161
164
|
reconcileMeetings: () => reconcileMeetings,
|
|
162
165
|
reconcileStripeSnapshot: () => reconcileStripeSnapshot,
|
|
163
166
|
refundedPatch: () => refundedPatch,
|
|
@@ -1679,8 +1682,8 @@ var APPLICATION_LABELS = {
|
|
|
1679
1682
|
state: "State"
|
|
1680
1683
|
};
|
|
1681
1684
|
function humanize(id6) {
|
|
1682
|
-
const
|
|
1683
|
-
return
|
|
1685
|
+
const text6 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1686
|
+
return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id6;
|
|
1684
1687
|
}
|
|
1685
1688
|
function applicationDefs(chapter) {
|
|
1686
1689
|
try {
|
|
@@ -1938,7 +1941,8 @@ function resolveNetwork(config, crm) {
|
|
|
1938
1941
|
sharedNotes,
|
|
1939
1942
|
editableFields,
|
|
1940
1943
|
stageTransitions,
|
|
1941
|
-
...target.admissionGrants === true ? { admissionGrants: true } : {}
|
|
1944
|
+
...target.admissionGrants === true ? { admissionGrants: true } : {},
|
|
1945
|
+
...target.commercialParity === true ? { commercialParity: true } : {}
|
|
1942
1946
|
});
|
|
1943
1947
|
}
|
|
1944
1948
|
const readerIds = /* @__PURE__ */ new Set();
|
|
@@ -1953,9 +1957,9 @@ function resolveNetwork(config, crm) {
|
|
|
1953
1957
|
readerIds.add(reader.id);
|
|
1954
1958
|
const readerFields = reader.fields ?? {};
|
|
1955
1959
|
const entries = Object.entries(readerFields);
|
|
1956
|
-
if (entries.length === 0 && !reader.admissionGrants) {
|
|
1960
|
+
if (entries.length === 0 && !reader.admissionGrants && !reader.commercialParity) {
|
|
1957
1961
|
throw new Error(
|
|
1958
|
-
`defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only reader`
|
|
1962
|
+
`defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only or commercial-parity reader`
|
|
1959
1963
|
);
|
|
1960
1964
|
}
|
|
1961
1965
|
for (const [type, names] of entries) {
|
|
@@ -2013,7 +2017,8 @@ function resolveNetwork(config, crm) {
|
|
|
2013
2017
|
sharedNotes,
|
|
2014
2018
|
editableFields,
|
|
2015
2019
|
stageTransitions,
|
|
2016
|
-
...reader.admissionGrants === true ? { admissionGrants: true } : {}
|
|
2020
|
+
...reader.admissionGrants === true ? { admissionGrants: true } : {},
|
|
2021
|
+
...reader.commercialParity === true ? { commercialParity: true } : {}
|
|
2017
2022
|
});
|
|
2018
2023
|
}
|
|
2019
2024
|
return { targets, readers };
|
|
@@ -2580,6 +2585,149 @@ function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
|
2580
2585
|
};
|
|
2581
2586
|
}
|
|
2582
2587
|
|
|
2588
|
+
// src/network-commercial-parity.ts
|
|
2589
|
+
var DIGEST = /^[0-9a-f]{64}$/;
|
|
2590
|
+
var MAX_RECORDS = 500;
|
|
2591
|
+
var text2 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
2592
|
+
function parityMessage(partition, kind, value) {
|
|
2593
|
+
return [
|
|
2594
|
+
"chapter-commercial-parity",
|
|
2595
|
+
"v1",
|
|
2596
|
+
partition.appId,
|
|
2597
|
+
partition.chapterId,
|
|
2598
|
+
partition.environment,
|
|
2599
|
+
partition.dataEnvironment,
|
|
2600
|
+
partition.runtime,
|
|
2601
|
+
kind,
|
|
2602
|
+
value
|
|
2603
|
+
].join("");
|
|
2604
|
+
}
|
|
2605
|
+
async function parityKey(secret) {
|
|
2606
|
+
const encoder4 = new TextEncoder();
|
|
2607
|
+
return crypto.subtle.importKey(
|
|
2608
|
+
"raw",
|
|
2609
|
+
encoder4.encode(secret),
|
|
2610
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2611
|
+
false,
|
|
2612
|
+
["sign"]
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
async function digestWithKey(key, partition, kind, value) {
|
|
2616
|
+
const encoder4 = new TextEncoder();
|
|
2617
|
+
const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
|
|
2618
|
+
return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2619
|
+
}
|
|
2620
|
+
async function commercialParityDigest(secret, partition, kind, value) {
|
|
2621
|
+
return digestWithKey(await parityKey(secret), partition, kind, value);
|
|
2622
|
+
}
|
|
2623
|
+
async function buildChapterCommercialParitySnapshot(input) {
|
|
2624
|
+
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
2625
|
+
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
2626
|
+
const eligible = input.rows.filter((row) => text2(row.id) && (text2(row.stripeCustomerId) || text2(row.stripeSubscriptionId)));
|
|
2627
|
+
const selected2 = eligible.slice(0, maxRecords);
|
|
2628
|
+
const key = await parityKey(input.secret);
|
|
2629
|
+
const records = await Promise.all(selected2.map(async (row) => {
|
|
2630
|
+
const applicationId = text2(row.id);
|
|
2631
|
+
const customerId = text2(row.stripeCustomerId);
|
|
2632
|
+
const subscriptionId = text2(row.stripeSubscriptionId);
|
|
2633
|
+
return {
|
|
2634
|
+
applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
|
|
2635
|
+
customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
|
|
2636
|
+
subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
|
|
2637
|
+
status: text2(row.status, 64),
|
|
2638
|
+
canceled: row.canceled === true
|
|
2639
|
+
};
|
|
2640
|
+
}));
|
|
2641
|
+
records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
|
|
2642
|
+
const truncated = input.scanTruncated || eligible.length > maxRecords;
|
|
2643
|
+
return {
|
|
2644
|
+
version: 1,
|
|
2645
|
+
site: { id: input.siteId },
|
|
2646
|
+
partition: input.partition,
|
|
2647
|
+
generatedAt: input.now ?? Date.now(),
|
|
2648
|
+
complete: !truncated,
|
|
2649
|
+
truncated,
|
|
2650
|
+
counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
|
|
2651
|
+
records
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
function recordKey(record4) {
|
|
2655
|
+
return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
|
|
2656
|
+
}
|
|
2657
|
+
function validSnapshot(snapshot, source) {
|
|
2658
|
+
const partition = snapshot?.partition;
|
|
2659
|
+
const records = snapshot?.records;
|
|
2660
|
+
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;
|
|
2661
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2662
|
+
for (const record4 of records) {
|
|
2663
|
+
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;
|
|
2664
|
+
const key = recordKey(record4);
|
|
2665
|
+
if (seen.has(key)) return false;
|
|
2666
|
+
seen.add(key);
|
|
2667
|
+
}
|
|
2668
|
+
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;
|
|
2669
|
+
}
|
|
2670
|
+
async function reconcileCommercialParitySnapshot(input) {
|
|
2671
|
+
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
2672
|
+
const sourceKey = networkCommercialSourceKey(input.source);
|
|
2673
|
+
const key = await parityKey(input.secret);
|
|
2674
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text2(row.applicationId) && text2(row.objectId)).map(async (row) => {
|
|
2675
|
+
const customerId = text2(row.customerId);
|
|
2676
|
+
const subscriptionId = text2(row.subscriptionId) ?? text2(row.objectId);
|
|
2677
|
+
return {
|
|
2678
|
+
applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
|
|
2679
|
+
customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
|
|
2680
|
+
subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
2681
|
+
status: null,
|
|
2682
|
+
canceled: false,
|
|
2683
|
+
stripeStatus: text2(row.status, 64),
|
|
2684
|
+
stripeCanceled: row.status === "canceled"
|
|
2685
|
+
};
|
|
2686
|
+
}));
|
|
2687
|
+
const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
|
|
2688
|
+
const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
|
|
2689
|
+
const exceptions2 = [];
|
|
2690
|
+
const records = [];
|
|
2691
|
+
for (const [key2, chapter] of chapterByKey) {
|
|
2692
|
+
const stripe = shadowByKey.get(key2);
|
|
2693
|
+
if (!stripe) {
|
|
2694
|
+
exceptions2.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
|
|
2695
|
+
continue;
|
|
2696
|
+
}
|
|
2697
|
+
const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
|
|
2698
|
+
records.push(compared);
|
|
2699
|
+
if (chapter.canceled !== stripe.stripeCanceled) {
|
|
2700
|
+
exceptions2.push({
|
|
2701
|
+
kind: "cancellation_mismatch",
|
|
2702
|
+
applicationDigest: chapter.applicationDigest,
|
|
2703
|
+
detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
for (const [key2, stripe] of shadowByKey) {
|
|
2708
|
+
if (!chapterByKey.has(key2)) {
|
|
2709
|
+
exceptions2.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
if (!input.snapshot.complete || input.snapshot.truncated) {
|
|
2713
|
+
exceptions2.push({
|
|
2714
|
+
kind: "chapter_truncated",
|
|
2715
|
+
detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
if (chapterByKey.size !== shadowByKey.size) {
|
|
2719
|
+
exceptions2.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
|
|
2720
|
+
}
|
|
2721
|
+
records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
|
|
2722
|
+
return {
|
|
2723
|
+
ok: input.snapshot.complete && exceptions2.length === 0,
|
|
2724
|
+
complete: input.snapshot.complete,
|
|
2725
|
+
counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
|
|
2726
|
+
records,
|
|
2727
|
+
exceptions: exceptions2
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2583
2731
|
// src/leader.ts
|
|
2584
2732
|
var import_crm3 = require("@odla-ai/crm");
|
|
2585
2733
|
function leaderCrmConfig(options = {}) {
|
|
@@ -2869,6 +3017,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2869
3017
|
path: "/api/network/shared-notes",
|
|
2870
3018
|
expectedStatus: 401
|
|
2871
3019
|
}] : [],
|
|
3020
|
+
...chapter.network.readers.some((reader) => reader.commercialParity) ? [{ path: "/api/network/commercial-parity", expectedStatus: 401 }] : [],
|
|
2872
3021
|
// Formation metadata is public only when the host explicitly enables
|
|
2873
3022
|
// the bounded intake contract.
|
|
2874
3023
|
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
|
|
@@ -3228,7 +3377,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
3228
3377
|
function unknownKeys2(value, allowed2, path, errors) {
|
|
3229
3378
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3230
3379
|
}
|
|
3231
|
-
function
|
|
3380
|
+
function text3(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
3232
3381
|
if (value === void 0 && !required) return void 0;
|
|
3233
3382
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
3234
3383
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -3254,7 +3403,7 @@ function scopeValue(value, path, errors) {
|
|
|
3254
3403
|
return { kind: raw2.kind };
|
|
3255
3404
|
}
|
|
3256
3405
|
if (raw2.kind === "chapter") {
|
|
3257
|
-
const chapterId =
|
|
3406
|
+
const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
3258
3407
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
3259
3408
|
return { kind: "chapter", chapterId };
|
|
3260
3409
|
}
|
|
@@ -3268,22 +3417,22 @@ function resourceValue(value, path, errors) {
|
|
|
3268
3417
|
return null;
|
|
3269
3418
|
}
|
|
3270
3419
|
unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
|
|
3271
|
-
const id6 =
|
|
3420
|
+
const id6 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
3272
3421
|
if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
3273
|
-
const kind =
|
|
3422
|
+
const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
3274
3423
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
3275
|
-
const title =
|
|
3276
|
-
const summary =
|
|
3277
|
-
const status =
|
|
3424
|
+
const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
3425
|
+
const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
3426
|
+
const status = text3(raw2.status, `${path}.status`, errors) ?? "";
|
|
3278
3427
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
3279
3428
|
const revision = integer2(raw2.revision, `${path}.revision`, errors);
|
|
3280
3429
|
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
3281
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
3430
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
3282
3431
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
3283
3432
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
3284
3433
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
3285
|
-
const bodyMarkdown =
|
|
3286
|
-
const url =
|
|
3434
|
+
const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
|
|
3435
|
+
const url = text3(raw2.url, `${path}.url`, errors, 2048, false);
|
|
3287
3436
|
if (url) {
|
|
3288
3437
|
try {
|
|
3289
3438
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -3295,9 +3444,9 @@ function resourceValue(value, path, errors) {
|
|
|
3295
3444
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
3296
3445
|
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
3297
3446
|
const brandAsset = asset ? {
|
|
3298
|
-
bookId:
|
|
3299
|
-
assetId:
|
|
3300
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
3447
|
+
bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
3448
|
+
assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
3449
|
+
...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
3301
3450
|
} : void 0;
|
|
3302
3451
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
3303
3452
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -3312,17 +3461,17 @@ function resourceValue(value, path, errors) {
|
|
|
3312
3461
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
3313
3462
|
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
3314
3463
|
const promotedFrom = promotedRaw ? {
|
|
3315
|
-
chapterId:
|
|
3316
|
-
resourceId:
|
|
3464
|
+
chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
3465
|
+
resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
3317
3466
|
resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
3318
3467
|
documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
3319
|
-
promotedBy:
|
|
3468
|
+
promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
3320
3469
|
promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
3321
3470
|
} : void 0;
|
|
3322
3471
|
const provenance = {
|
|
3323
|
-
createdBy:
|
|
3472
|
+
createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
3324
3473
|
createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
3325
|
-
updatedBy:
|
|
3474
|
+
updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
3326
3475
|
updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
3327
3476
|
...promotedFrom ? { promotedFrom } : {}
|
|
3328
3477
|
};
|
|
@@ -3341,7 +3490,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
3341
3490
|
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
3342
3491
|
const sourceType = sourceRaw?.type;
|
|
3343
3492
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
3344
|
-
const sourceAppId =
|
|
3493
|
+
const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
3345
3494
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3346
3495
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
3347
3496
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -3349,8 +3498,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
3349
3498
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
3350
3499
|
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
3351
3500
|
const target = targetRaw ? {
|
|
3352
|
-
appId:
|
|
3353
|
-
environment:
|
|
3501
|
+
appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
3502
|
+
environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
3354
3503
|
} : void 0;
|
|
3355
3504
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
3356
3505
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -3474,10 +3623,10 @@ function planDelivery(input) {
|
|
|
3474
3623
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
3475
3624
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
3476
3625
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
3477
|
-
const
|
|
3626
|
+
const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
3478
3627
|
|
|
3479
3628
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
3480
|
-
return { deliver: true, transport, to, subject, text:
|
|
3629
|
+
return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
|
|
3481
3630
|
}
|
|
3482
3631
|
|
|
3483
3632
|
// src/notify.ts
|
|
@@ -3883,7 +4032,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
3883
4032
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
3884
4033
|
var CURRENCY = /^[a-z]{3}$/;
|
|
3885
4034
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3886
|
-
var
|
|
4035
|
+
var text4 = (value, path, errors, max) => {
|
|
3887
4036
|
if (typeof value !== "string" || !value.trim()) {
|
|
3888
4037
|
errors.push(`${path} is required`);
|
|
3889
4038
|
return "";
|
|
@@ -3911,23 +4060,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3911
4060
|
}
|
|
3912
4061
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
3913
4062
|
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3914
|
-
const id6 =
|
|
4063
|
+
const id6 = text4(item.id, `${path}.id`, errors, 80);
|
|
3915
4064
|
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3916
4065
|
if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
3917
4066
|
ids.add(id6);
|
|
3918
4067
|
const targetRaw = record3(item.target);
|
|
3919
4068
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
3920
4069
|
if (targetRaw?.type === "tier") {
|
|
3921
|
-
const tierId =
|
|
4070
|
+
const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
3922
4071
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
3923
4072
|
target = { type: "tier", tierId };
|
|
3924
4073
|
} else if (targetRaw?.type === "entitlement") {
|
|
3925
|
-
const entitlementKey =
|
|
4074
|
+
const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
3926
4075
|
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
3927
4076
|
target = { type: "entitlement", entitlementKey };
|
|
3928
4077
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
3929
4078
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
3930
|
-
const stripePriceId =
|
|
4079
|
+
const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
3931
4080
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
3932
4081
|
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`);
|
|
3933
4082
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -3949,9 +4098,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3949
4098
|
redemptionLifetimeHours: lifetime,
|
|
3950
4099
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
3951
4100
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
3952
|
-
title:
|
|
3953
|
-
description:
|
|
3954
|
-
ctaLabel:
|
|
4101
|
+
title: text4(item.title, `${path}.title`, errors, 160),
|
|
4102
|
+
description: text4(item.description, `${path}.description`, errors, 2e3),
|
|
4103
|
+
ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
3955
4104
|
});
|
|
3956
4105
|
}
|
|
3957
4106
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -4873,10 +5022,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
4873
5022
|
errors.push(`${path} must be a string`);
|
|
4874
5023
|
return void 0;
|
|
4875
5024
|
}
|
|
4876
|
-
const
|
|
4877
|
-
if (required && !
|
|
4878
|
-
if (
|
|
4879
|
-
return
|
|
5025
|
+
const text6 = value.trim();
|
|
5026
|
+
if (required && !text6) errors.push(`${path} is required`);
|
|
5027
|
+
if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
5028
|
+
return text6;
|
|
4880
5029
|
}
|
|
4881
5030
|
function validateMembershipPageDocument(input, options = {}) {
|
|
4882
5031
|
const errors = [];
|
|
@@ -4925,8 +5074,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
4925
5074
|
else {
|
|
4926
5075
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
4927
5076
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
4928
|
-
const
|
|
4929
|
-
return
|
|
5077
|
+
const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
5078
|
+
return text6 ? [text6] : [];
|
|
4930
5079
|
});
|
|
4931
5080
|
}
|
|
4932
5081
|
}
|
|
@@ -5025,7 +5174,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
5025
5174
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
5026
5175
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
5027
5176
|
}
|
|
5028
|
-
function
|
|
5177
|
+
function text5(value, path, errors, max = 65536) {
|
|
5029
5178
|
if (typeof value !== "string" || !value.trim()) {
|
|
5030
5179
|
errors.push(`${path} is required`);
|
|
5031
5180
|
return "";
|
|
@@ -5048,11 +5197,11 @@ function tiersOf(value, errors) {
|
|
|
5048
5197
|
return [];
|
|
5049
5198
|
}
|
|
5050
5199
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
5051
|
-
const id6 =
|
|
5200
|
+
const id6 = text5(item.id, `${path}.id`, errors, 80);
|
|
5052
5201
|
if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5053
5202
|
if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
5054
5203
|
seen.add(id6);
|
|
5055
|
-
const name =
|
|
5204
|
+
const name = text5(item.name, `${path}.name`, errors, 160);
|
|
5056
5205
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
5057
5206
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
5058
5207
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -5064,7 +5213,7 @@ function tiersOf(value, errors) {
|
|
|
5064
5213
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
5065
5214
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
5066
5215
|
}
|
|
5067
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
5216
|
+
const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
5068
5217
|
return [{
|
|
5069
5218
|
id: id6,
|
|
5070
5219
|
name,
|
|
@@ -5120,7 +5269,7 @@ function policyOf(value, errors) {
|
|
|
5120
5269
|
const result = {};
|
|
5121
5270
|
for (const key of allowed2) {
|
|
5122
5271
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
5123
|
-
result[key] =
|
|
5272
|
+
result[key] = text5(input[key], `document.policy.${key}`, errors);
|
|
5124
5273
|
}
|
|
5125
5274
|
return result;
|
|
5126
5275
|
}
|
|
@@ -5158,9 +5307,9 @@ function validateSignupControlDocument(input) {
|
|
|
5158
5307
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
5159
5308
|
const source = recordOf(root.source);
|
|
5160
5309
|
const target = recordOf(root.target);
|
|
5161
|
-
const appId =
|
|
5162
|
-
const chapterId =
|
|
5163
|
-
const environment =
|
|
5310
|
+
const appId = text5(source?.appId, "document.source.appId", errors, 63);
|
|
5311
|
+
const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
5312
|
+
const environment = text5(target?.environment, "document.target.environment", errors, 63);
|
|
5164
5313
|
if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
5165
5314
|
if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
5166
5315
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -5204,7 +5353,7 @@ function validateSignupControlDocument(input) {
|
|
|
5204
5353
|
var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
|
|
5205
5354
|
var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
|
|
5206
5355
|
var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
|
|
5207
|
-
var
|
|
5356
|
+
var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
|
|
5208
5357
|
var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
|
|
5209
5358
|
var MAX_TOKENS = 256;
|
|
5210
5359
|
var MAX_VALUE_LENGTH = 1e3;
|
|
@@ -5248,7 +5397,7 @@ function stateFor(revision, options) {
|
|
|
5248
5397
|
metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
|
|
5249
5398
|
};
|
|
5250
5399
|
}
|
|
5251
|
-
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !
|
|
5400
|
+
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
|
|
5252
5401
|
throw new TypeError("signup-control theme revision metadata is invalid");
|
|
5253
5402
|
}
|
|
5254
5403
|
return {
|
|
@@ -5337,9 +5486,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
5337
5486
|
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
5338
5487
|
continue;
|
|
5339
5488
|
}
|
|
5340
|
-
const
|
|
5341
|
-
if (
|
|
5342
|
-
if (
|
|
5489
|
+
const text6 = (match[1] ?? "").trim();
|
|
5490
|
+
if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
|
|
5491
|
+
if (text6) benefits.push(text6);
|
|
5343
5492
|
}
|
|
5344
5493
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
5345
5494
|
return benefits.slice(0, MAX_BENEFITS2);
|