@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/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
|
});
|
|
@@ -1493,8 +1511,10 @@ These bite silently — a smoke test won't catch them:
|
|
|
1493
1511
|
- **Bootstrap the first admin according to the selected auth source.** Chapter
|
|
1494
1512
|
mode defaults to Clerk claims: set the first operator's Clerk
|
|
1495
1513
|
`public_metadata.role` to `admin`. If that operator also needs the
|
|
1496
|
-
|
|
1497
|
-
|
|
1514
|
+
super-admin tier, prefer `auth: { superAdminSource: "odla" }` and grant their
|
|
1515
|
+
app-scoped role on Database → Users in odla Studio. Existing sites retain the
|
|
1516
|
+
legacy `superAdmins` table until they opt in. Hub mode defaults to table auth:
|
|
1517
|
+
add the first operator's lowercase
|
|
1498
1518
|
email to `admins` in Studio. Neither allowlist is ever written by a worker
|
|
1499
1519
|
route or provisioning seed; that is deliberate, so the running app cannot
|
|
1500
1520
|
grant itself admin. Confirm the result by signing in, not merely by inspecting
|
|
@@ -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 {
|
|
@@ -449,7 +458,7 @@ interface ChapterScheduling {
|
|
|
449
458
|
*
|
|
450
459
|
* - `"claim"` reads a role ladder from a JWT claim (the member-site model:
|
|
451
460
|
* `provisional → member → admin`). Admin is the last (highest) rung; the
|
|
452
|
-
*
|
|
461
|
+
* Studio-managed super-admin tier sits above it.
|
|
453
462
|
* - `"table"` gates on the deny-all `admins` allowlist (the hub model):
|
|
454
463
|
* a binary "email is an admin or isn't", set only in odla Studio.
|
|
455
464
|
*
|
|
@@ -461,10 +470,13 @@ interface ChapterAuth {
|
|
|
461
470
|
/** Role ladder low→high, for `source: "claim"`. The last entry is admin.
|
|
462
471
|
* Default `["provisional", "member", "admin"]`. */
|
|
463
472
|
ladder?: readonly string[];
|
|
464
|
-
/**
|
|
465
|
-
* create/modify admins
|
|
466
|
-
* odla Studio. Default: `true` for `"claim"`, `false` for `"table"`. */
|
|
473
|
+
/** Enable the Studio-managed super-admin tier — the only tier that may
|
|
474
|
+
* create/modify admins. Default: `true` for `"claim"`, `false` for `"table"`. */
|
|
467
475
|
superAdmins?: boolean;
|
|
476
|
+
/** Authority behind the super-admin tier. `"table"` preserves the legacy
|
|
477
|
+
* app allowlist; `"odla"` reads protected `$users.role`. Default `"table"`
|
|
478
|
+
* for backward compatibility. */
|
|
479
|
+
superAdminSource?: "table" | "odla";
|
|
468
480
|
}
|
|
469
481
|
/** The fully-resolved auth policy (defaults applied) carried on the {@link Chapter}. */
|
|
470
482
|
interface ResolvedAuth {
|
|
@@ -474,6 +486,7 @@ interface ResolvedAuth {
|
|
|
474
486
|
/** The highest ladder rung (last entry) — the "admin" gate. */
|
|
475
487
|
adminRole: string;
|
|
476
488
|
superAdmins: boolean;
|
|
489
|
+
superAdminSource: "none" | "table" | "odla";
|
|
477
490
|
}
|
|
478
491
|
/** The application status pipeline. Which statuses exist, and the subsets a site
|
|
479
492
|
* allows a call to be booked from / an application approved from. Defaults to
|
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,
|
|
@@ -1012,7 +1015,7 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
1012
1015
|
}
|
|
1013
1016
|
}
|
|
1014
1017
|
if (auth.source === "table") entities.admins = admins;
|
|
1015
|
-
if (auth.
|
|
1018
|
+
if (auth.superAdminSource === "table") entities.superAdmins = superAdmins;
|
|
1016
1019
|
if (includeNetworkNotes) entities.networkNotes = networkNotes;
|
|
1017
1020
|
if (includeBuilderLibrary) {
|
|
1018
1021
|
entities.builderLibraryControls = builderLibraryControls;
|
|
@@ -1195,7 +1198,12 @@ function resolveAuth(mode, auth) {
|
|
|
1195
1198
|
}
|
|
1196
1199
|
const adminRole = ladder[ladder.length - 1];
|
|
1197
1200
|
const superAdmins2 = a.superAdmins ?? source === "claim";
|
|
1198
|
-
|
|
1201
|
+
const configuredSuperSource = a.superAdminSource ?? "table";
|
|
1202
|
+
if (configuredSuperSource !== "table" && configuredSuperSource !== "odla") {
|
|
1203
|
+
throw new Error(`defineChapter.auth.superAdminSource: must be "table" or "odla" \u2014 got ${JSON.stringify(a.superAdminSource)}`);
|
|
1204
|
+
}
|
|
1205
|
+
const superAdminSource = superAdmins2 ? configuredSuperSource : "none";
|
|
1206
|
+
return { source, claim, ladder, adminRole, superAdmins: superAdmins2, superAdminSource };
|
|
1199
1207
|
}
|
|
1200
1208
|
function roleFromClaim(payload, auth) {
|
|
1201
1209
|
const raw2 = payload[auth.claim];
|
|
@@ -1679,8 +1687,8 @@ var APPLICATION_LABELS = {
|
|
|
1679
1687
|
state: "State"
|
|
1680
1688
|
};
|
|
1681
1689
|
function humanize(id6) {
|
|
1682
|
-
const
|
|
1683
|
-
return
|
|
1690
|
+
const text6 = id6.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1691
|
+
return text6 ? `${text6[0].toUpperCase()}${text6.slice(1)}` : id6;
|
|
1684
1692
|
}
|
|
1685
1693
|
function applicationDefs(chapter) {
|
|
1686
1694
|
try {
|
|
@@ -1938,7 +1946,8 @@ function resolveNetwork(config, crm) {
|
|
|
1938
1946
|
sharedNotes,
|
|
1939
1947
|
editableFields,
|
|
1940
1948
|
stageTransitions,
|
|
1941
|
-
...target.admissionGrants === true ? { admissionGrants: true } : {}
|
|
1949
|
+
...target.admissionGrants === true ? { admissionGrants: true } : {},
|
|
1950
|
+
...target.commercialParity === true ? { commercialParity: true } : {}
|
|
1942
1951
|
});
|
|
1943
1952
|
}
|
|
1944
1953
|
const readerIds = /* @__PURE__ */ new Set();
|
|
@@ -1953,9 +1962,9 @@ function resolveNetwork(config, crm) {
|
|
|
1953
1962
|
readerIds.add(reader.id);
|
|
1954
1963
|
const readerFields = reader.fields ?? {};
|
|
1955
1964
|
const entries = Object.entries(readerFields);
|
|
1956
|
-
if (entries.length === 0 && !reader.admissionGrants) {
|
|
1965
|
+
if (entries.length === 0 && !reader.admissionGrants && !reader.commercialParity) {
|
|
1957
1966
|
throw new Error(
|
|
1958
|
-
`defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only reader`
|
|
1967
|
+
`defineChapter.network.readers.${reader.id}.fields: at least one CRM type is required unless this is an admission-only or commercial-parity reader`
|
|
1959
1968
|
);
|
|
1960
1969
|
}
|
|
1961
1970
|
for (const [type, names] of entries) {
|
|
@@ -2013,7 +2022,8 @@ function resolveNetwork(config, crm) {
|
|
|
2013
2022
|
sharedNotes,
|
|
2014
2023
|
editableFields,
|
|
2015
2024
|
stageTransitions,
|
|
2016
|
-
...reader.admissionGrants === true ? { admissionGrants: true } : {}
|
|
2025
|
+
...reader.admissionGrants === true ? { admissionGrants: true } : {},
|
|
2026
|
+
...reader.commercialParity === true ? { commercialParity: true } : {}
|
|
2017
2027
|
});
|
|
2018
2028
|
}
|
|
2019
2029
|
return { targets, readers };
|
|
@@ -2580,6 +2590,149 @@ function reconcileStripeSnapshot(shadow, snapshots, sources) {
|
|
|
2580
2590
|
};
|
|
2581
2591
|
}
|
|
2582
2592
|
|
|
2593
|
+
// src/network-commercial-parity.ts
|
|
2594
|
+
var DIGEST = /^[0-9a-f]{64}$/;
|
|
2595
|
+
var MAX_RECORDS = 500;
|
|
2596
|
+
var text2 = (value, max = 255) => typeof value === "string" && value.length > 0 && value.length <= max ? value : null;
|
|
2597
|
+
function parityMessage(partition, kind, value) {
|
|
2598
|
+
return [
|
|
2599
|
+
"chapter-commercial-parity",
|
|
2600
|
+
"v1",
|
|
2601
|
+
partition.appId,
|
|
2602
|
+
partition.chapterId,
|
|
2603
|
+
partition.environment,
|
|
2604
|
+
partition.dataEnvironment,
|
|
2605
|
+
partition.runtime,
|
|
2606
|
+
kind,
|
|
2607
|
+
value
|
|
2608
|
+
].join("");
|
|
2609
|
+
}
|
|
2610
|
+
async function parityKey(secret) {
|
|
2611
|
+
const encoder4 = new TextEncoder();
|
|
2612
|
+
return crypto.subtle.importKey(
|
|
2613
|
+
"raw",
|
|
2614
|
+
encoder4.encode(secret),
|
|
2615
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2616
|
+
false,
|
|
2617
|
+
["sign"]
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
async function digestWithKey(key, partition, kind, value) {
|
|
2621
|
+
const encoder4 = new TextEncoder();
|
|
2622
|
+
const signed = await crypto.subtle.sign("HMAC", key, encoder4.encode(parityMessage(partition, kind, value)));
|
|
2623
|
+
return [...new Uint8Array(signed)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2624
|
+
}
|
|
2625
|
+
async function commercialParityDigest(secret, partition, kind, value) {
|
|
2626
|
+
return digestWithKey(await parityKey(secret), partition, kind, value);
|
|
2627
|
+
}
|
|
2628
|
+
async function buildChapterCommercialParitySnapshot(input) {
|
|
2629
|
+
const requestedMax = input.maxRecords ?? MAX_RECORDS;
|
|
2630
|
+
const maxRecords = Number.isSafeInteger(requestedMax) ? Math.max(0, Math.min(requestedMax, MAX_RECORDS)) : MAX_RECORDS;
|
|
2631
|
+
const eligible = input.rows.filter((row) => text2(row.id) && (text2(row.stripeCustomerId) || text2(row.stripeSubscriptionId)));
|
|
2632
|
+
const selected2 = eligible.slice(0, maxRecords);
|
|
2633
|
+
const key = await parityKey(input.secret);
|
|
2634
|
+
const records = await Promise.all(selected2.map(async (row) => {
|
|
2635
|
+
const applicationId = text2(row.id);
|
|
2636
|
+
const customerId = text2(row.stripeCustomerId);
|
|
2637
|
+
const subscriptionId = text2(row.stripeSubscriptionId);
|
|
2638
|
+
return {
|
|
2639
|
+
applicationDigest: await digestWithKey(key, input.partition, "application", applicationId),
|
|
2640
|
+
customerDigest: customerId ? await digestWithKey(key, input.partition, "customer", customerId) : null,
|
|
2641
|
+
subscriptionDigest: subscriptionId ? await digestWithKey(key, input.partition, "subscription", subscriptionId) : null,
|
|
2642
|
+
status: text2(row.status, 64),
|
|
2643
|
+
canceled: row.canceled === true
|
|
2644
|
+
};
|
|
2645
|
+
}));
|
|
2646
|
+
records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
|
|
2647
|
+
const truncated = input.scanTruncated || eligible.length > maxRecords;
|
|
2648
|
+
return {
|
|
2649
|
+
version: 1,
|
|
2650
|
+
site: { id: input.siteId },
|
|
2651
|
+
partition: input.partition,
|
|
2652
|
+
generatedAt: input.now ?? Date.now(),
|
|
2653
|
+
complete: !truncated,
|
|
2654
|
+
truncated,
|
|
2655
|
+
counts: { scanned: input.scanned, eligible: eligible.length, returned: records.length },
|
|
2656
|
+
records
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
function recordKey(record4) {
|
|
2660
|
+
return `${record4.applicationDigest}:${record4.customerDigest ?? "-"}:${record4.subscriptionDigest ?? "-"}`;
|
|
2661
|
+
}
|
|
2662
|
+
function validSnapshot(snapshot, source) {
|
|
2663
|
+
const partition = snapshot?.partition;
|
|
2664
|
+
const records = snapshot?.records;
|
|
2665
|
+
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;
|
|
2666
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2667
|
+
for (const record4 of records) {
|
|
2668
|
+
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;
|
|
2669
|
+
const key = recordKey(record4);
|
|
2670
|
+
if (seen.has(key)) return false;
|
|
2671
|
+
seen.add(key);
|
|
2672
|
+
}
|
|
2673
|
+
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;
|
|
2674
|
+
}
|
|
2675
|
+
async function reconcileCommercialParitySnapshot(input) {
|
|
2676
|
+
if (!validSnapshot(input.snapshot, input.source)) throw new Error("invalid chapter commercial parity snapshot");
|
|
2677
|
+
const sourceKey = networkCommercialSourceKey(input.source);
|
|
2678
|
+
const key = await parityKey(input.secret);
|
|
2679
|
+
const shadow = await Promise.all(input.projection.subscriptions.filter((row) => row.sourceKey === sourceKey && text2(row.applicationId) && text2(row.objectId)).map(async (row) => {
|
|
2680
|
+
const customerId = text2(row.customerId);
|
|
2681
|
+
const subscriptionId = text2(row.subscriptionId) ?? text2(row.objectId);
|
|
2682
|
+
return {
|
|
2683
|
+
applicationDigest: await digestWithKey(key, input.snapshot.partition, "application", row.applicationId),
|
|
2684
|
+
customerDigest: customerId ? await digestWithKey(key, input.snapshot.partition, "customer", customerId) : null,
|
|
2685
|
+
subscriptionDigest: subscriptionId ? await digestWithKey(key, input.snapshot.partition, "subscription", subscriptionId) : null,
|
|
2686
|
+
status: null,
|
|
2687
|
+
canceled: false,
|
|
2688
|
+
stripeStatus: text2(row.status, 64),
|
|
2689
|
+
stripeCanceled: row.status === "canceled"
|
|
2690
|
+
};
|
|
2691
|
+
}));
|
|
2692
|
+
const chapterByKey = new Map(input.snapshot.records.map((record4) => [recordKey(record4), record4]));
|
|
2693
|
+
const shadowByKey = new Map(shadow.map((record4) => [recordKey(record4), record4]));
|
|
2694
|
+
const exceptions2 = [];
|
|
2695
|
+
const records = [];
|
|
2696
|
+
for (const [key2, chapter] of chapterByKey) {
|
|
2697
|
+
const stripe = shadowByKey.get(key2);
|
|
2698
|
+
if (!stripe) {
|
|
2699
|
+
exceptions2.push({ kind: "chapter_only", applicationDigest: chapter.applicationDigest, detail: { chapterStatus: chapter.status } });
|
|
2700
|
+
continue;
|
|
2701
|
+
}
|
|
2702
|
+
const compared = { ...chapter, stripeStatus: stripe.stripeStatus, stripeCanceled: stripe.stripeCanceled };
|
|
2703
|
+
records.push(compared);
|
|
2704
|
+
if (chapter.canceled !== stripe.stripeCanceled) {
|
|
2705
|
+
exceptions2.push({
|
|
2706
|
+
kind: "cancellation_mismatch",
|
|
2707
|
+
applicationDigest: chapter.applicationDigest,
|
|
2708
|
+
detail: { chapterCanceled: chapter.canceled, stripeCanceled: stripe.stripeCanceled }
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
for (const [key2, stripe] of shadowByKey) {
|
|
2713
|
+
if (!chapterByKey.has(key2)) {
|
|
2714
|
+
exceptions2.push({ kind: "shadow_only", applicationDigest: stripe.applicationDigest, detail: { stripeStatus: stripe.stripeStatus } });
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (!input.snapshot.complete || input.snapshot.truncated) {
|
|
2718
|
+
exceptions2.push({
|
|
2719
|
+
kind: "chapter_truncated",
|
|
2720
|
+
detail: { scanned: input.snapshot.counts.scanned, eligible: input.snapshot.counts.eligible, returned: input.snapshot.counts.returned }
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
if (chapterByKey.size !== shadowByKey.size) {
|
|
2724
|
+
exceptions2.push({ kind: "count_mismatch", detail: { chapter: chapterByKey.size, shadow: shadowByKey.size } });
|
|
2725
|
+
}
|
|
2726
|
+
records.sort((left, right) => left.applicationDigest.localeCompare(right.applicationDigest));
|
|
2727
|
+
return {
|
|
2728
|
+
ok: input.snapshot.complete && exceptions2.length === 0,
|
|
2729
|
+
complete: input.snapshot.complete,
|
|
2730
|
+
counts: { chapter: chapterByKey.size, shadow: shadowByKey.size, matched: records.length },
|
|
2731
|
+
records,
|
|
2732
|
+
exceptions: exceptions2
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2583
2736
|
// src/leader.ts
|
|
2584
2737
|
var import_crm3 = require("@odla-ai/crm");
|
|
2585
2738
|
function leaderCrmConfig(options = {}) {
|
|
@@ -2869,6 +3022,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2869
3022
|
path: "/api/network/shared-notes",
|
|
2870
3023
|
expectedStatus: 401
|
|
2871
3024
|
}] : [],
|
|
3025
|
+
...chapter.network.readers.some((reader) => reader.commercialParity) ? [{ path: "/api/network/commercial-parity", expectedStatus: 401 }] : [],
|
|
2872
3026
|
// Formation metadata is public only when the host explicitly enables
|
|
2873
3027
|
// the bounded intake contract.
|
|
2874
3028
|
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
|
|
@@ -3228,7 +3382,7 @@ var record2 = (value) => value !== null && typeof value === "object" && !Array.i
|
|
|
3228
3382
|
function unknownKeys2(value, allowed2, path, errors) {
|
|
3229
3383
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3230
3384
|
}
|
|
3231
|
-
function
|
|
3385
|
+
function text3(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
3232
3386
|
if (value === void 0 && !required) return void 0;
|
|
3233
3387
|
if (typeof value !== "string" || required && !value.trim()) {
|
|
3234
3388
|
errors.push(`${path} must be a non-empty string`);
|
|
@@ -3254,7 +3408,7 @@ function scopeValue(value, path, errors) {
|
|
|
3254
3408
|
return { kind: raw2.kind };
|
|
3255
3409
|
}
|
|
3256
3410
|
if (raw2.kind === "chapter") {
|
|
3257
|
-
const chapterId =
|
|
3411
|
+
const chapterId = text3(raw2.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
3258
3412
|
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
3259
3413
|
return { kind: "chapter", chapterId };
|
|
3260
3414
|
}
|
|
@@ -3268,22 +3422,22 @@ function resourceValue(value, path, errors) {
|
|
|
3268
3422
|
return null;
|
|
3269
3423
|
}
|
|
3270
3424
|
unknownKeys2(raw2, RESOURCE_KEYS, path, errors);
|
|
3271
|
-
const id6 =
|
|
3425
|
+
const id6 = text3(raw2.id, `${path}.id`, errors, 80) ?? "";
|
|
3272
3426
|
if (id6 && !ID2.test(id6)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
3273
|
-
const kind =
|
|
3427
|
+
const kind = text3(raw2.kind, `${path}.kind`, errors) ?? "";
|
|
3274
3428
|
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
3275
|
-
const title =
|
|
3276
|
-
const summary =
|
|
3277
|
-
const status =
|
|
3429
|
+
const title = text3(raw2.title, `${path}.title`, errors, 240) ?? "";
|
|
3430
|
+
const summary = text3(raw2.summary, `${path}.summary`, errors, 1e3, false);
|
|
3431
|
+
const status = text3(raw2.status, `${path}.status`, errors) ?? "";
|
|
3278
3432
|
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
3279
3433
|
const revision = integer2(raw2.revision, `${path}.revision`, errors);
|
|
3280
3434
|
const scope = scopeValue(raw2.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
3281
|
-
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) =>
|
|
3435
|
+
const tags = Array.isArray(raw2.tags) ? raw2.tags.map((tag, index2) => text3(tag, `${path}.tags[${index2}]`, errors, 80) ?? "") : [];
|
|
3282
3436
|
if (!Array.isArray(raw2.tags)) errors.push(`${path}.tags must be an array`);
|
|
3283
3437
|
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
3284
3438
|
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
3285
|
-
const bodyMarkdown =
|
|
3286
|
-
const url =
|
|
3439
|
+
const bodyMarkdown = text3(raw2.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
|
|
3440
|
+
const url = text3(raw2.url, `${path}.url`, errors, 2048, false);
|
|
3287
3441
|
if (url) {
|
|
3288
3442
|
try {
|
|
3289
3443
|
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
@@ -3295,9 +3449,9 @@ function resourceValue(value, path, errors) {
|
|
|
3295
3449
|
if (raw2.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
3296
3450
|
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
3297
3451
|
const brandAsset = asset ? {
|
|
3298
|
-
bookId:
|
|
3299
|
-
assetId:
|
|
3300
|
-
...asset.filename === void 0 ? {} : { filename:
|
|
3452
|
+
bookId: text3(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
3453
|
+
assetId: text3(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
3454
|
+
...asset.filename === void 0 ? {} : { filename: text3(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
3301
3455
|
} : void 0;
|
|
3302
3456
|
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
3303
3457
|
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
@@ -3312,17 +3466,17 @@ function resourceValue(value, path, errors) {
|
|
|
3312
3466
|
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
3313
3467
|
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
3314
3468
|
const promotedFrom = promotedRaw ? {
|
|
3315
|
-
chapterId:
|
|
3316
|
-
resourceId:
|
|
3469
|
+
chapterId: text3(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
3470
|
+
resourceId: text3(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
3317
3471
|
resourceRevision: integer2(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
3318
3472
|
documentRevision: integer2(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
3319
|
-
promotedBy:
|
|
3473
|
+
promotedBy: text3(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
3320
3474
|
promotedAt: integer2(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
3321
3475
|
} : void 0;
|
|
3322
3476
|
const provenance = {
|
|
3323
|
-
createdBy:
|
|
3477
|
+
createdBy: text3(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
3324
3478
|
createdAt: integer2(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
3325
|
-
updatedBy:
|
|
3479
|
+
updatedBy: text3(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
3326
3480
|
updatedAt: integer2(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
3327
3481
|
...promotedFrom ? { promotedFrom } : {}
|
|
3328
3482
|
};
|
|
@@ -3341,7 +3495,7 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
3341
3495
|
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
3342
3496
|
const sourceType = sourceRaw?.type;
|
|
3343
3497
|
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
3344
|
-
const sourceAppId =
|
|
3498
|
+
const sourceAppId = text3(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
3345
3499
|
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3346
3500
|
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
3347
3501
|
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
@@ -3349,8 +3503,8 @@ function validateBuilderLibraryDocument(value, options = {}) {
|
|
|
3349
3503
|
if (raw2.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
3350
3504
|
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
3351
3505
|
const target = targetRaw ? {
|
|
3352
|
-
appId:
|
|
3353
|
-
environment:
|
|
3506
|
+
appId: text3(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
3507
|
+
environment: text3(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
3354
3508
|
} : void 0;
|
|
3355
3509
|
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
3356
3510
|
if (!Array.isArray(raw2.resources)) errors.push("document.resources must be an array");
|
|
@@ -3474,10 +3628,10 @@ function planDelivery(input) {
|
|
|
3474
3628
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
3475
3629
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
3476
3630
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
3477
|
-
const
|
|
3631
|
+
const text6 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
3478
3632
|
|
|
3479
3633
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
3480
|
-
return { deliver: true, transport, to, subject, text:
|
|
3634
|
+
return { deliver: true, transport, to, subject, text: text6, redirected: redirect };
|
|
3481
3635
|
}
|
|
3482
3636
|
|
|
3483
3637
|
// src/notify.ts
|
|
@@ -3883,7 +4037,7 @@ var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
|
3883
4037
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
3884
4038
|
var CURRENCY = /^[a-z]{3}$/;
|
|
3885
4039
|
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3886
|
-
var
|
|
4040
|
+
var text4 = (value, path, errors, max) => {
|
|
3887
4041
|
if (typeof value !== "string" || !value.trim()) {
|
|
3888
4042
|
errors.push(`${path} is required`);
|
|
3889
4043
|
return "";
|
|
@@ -3911,23 +4065,23 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3911
4065
|
}
|
|
3912
4066
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
3913
4067
|
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3914
|
-
const id6 =
|
|
4068
|
+
const id6 = text4(item.id, `${path}.id`, errors, 80);
|
|
3915
4069
|
if (id6 && !ID4.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3916
4070
|
if (ids.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
3917
4071
|
ids.add(id6);
|
|
3918
4072
|
const targetRaw = record3(item.target);
|
|
3919
4073
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
3920
4074
|
if (targetRaw?.type === "tier") {
|
|
3921
|
-
const tierId =
|
|
4075
|
+
const tierId = text4(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
3922
4076
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
3923
4077
|
target = { type: "tier", tierId };
|
|
3924
4078
|
} else if (targetRaw?.type === "entitlement") {
|
|
3925
|
-
const entitlementKey =
|
|
4079
|
+
const entitlementKey = text4(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
3926
4080
|
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
3927
4081
|
target = { type: "entitlement", entitlementKey };
|
|
3928
4082
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
3929
4083
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
3930
|
-
const stripePriceId =
|
|
4084
|
+
const stripePriceId = text4(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
3931
4085
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
3932
4086
|
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
4087
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -3949,9 +4103,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
3949
4103
|
redemptionLifetimeHours: lifetime,
|
|
3950
4104
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
3951
4105
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
3952
|
-
title:
|
|
3953
|
-
description:
|
|
3954
|
-
ctaLabel:
|
|
4106
|
+
title: text4(item.title, `${path}.title`, errors, 160),
|
|
4107
|
+
description: text4(item.description, `${path}.description`, errors, 2e3),
|
|
4108
|
+
ctaLabel: text4(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
3955
4109
|
});
|
|
3956
4110
|
}
|
|
3957
4111
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -4873,10 +5027,10 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
4873
5027
|
errors.push(`${path} must be a string`);
|
|
4874
5028
|
return void 0;
|
|
4875
5029
|
}
|
|
4876
|
-
const
|
|
4877
|
-
if (required && !
|
|
4878
|
-
if (
|
|
4879
|
-
return
|
|
5030
|
+
const text6 = value.trim();
|
|
5031
|
+
if (required && !text6) errors.push(`${path} is required`);
|
|
5032
|
+
if (text6.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
5033
|
+
return text6;
|
|
4880
5034
|
}
|
|
4881
5035
|
function validateMembershipPageDocument(input, options = {}) {
|
|
4882
5036
|
const errors = [];
|
|
@@ -4925,8 +5079,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
4925
5079
|
else {
|
|
4926
5080
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
4927
5081
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
4928
|
-
const
|
|
4929
|
-
return
|
|
5082
|
+
const text6 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
5083
|
+
return text6 ? [text6] : [];
|
|
4930
5084
|
});
|
|
4931
5085
|
}
|
|
4932
5086
|
}
|
|
@@ -5025,7 +5179,7 @@ var recordOf = (value) => value !== null && typeof value === "object" && !Array.
|
|
|
5025
5179
|
function unknownKeys3(value, allowed2, path, errors) {
|
|
5026
5180
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
5027
5181
|
}
|
|
5028
|
-
function
|
|
5182
|
+
function text5(value, path, errors, max = 65536) {
|
|
5029
5183
|
if (typeof value !== "string" || !value.trim()) {
|
|
5030
5184
|
errors.push(`${path} is required`);
|
|
5031
5185
|
return "";
|
|
@@ -5048,11 +5202,11 @@ function tiersOf(value, errors) {
|
|
|
5048
5202
|
return [];
|
|
5049
5203
|
}
|
|
5050
5204
|
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
5051
|
-
const id6 =
|
|
5205
|
+
const id6 = text5(item.id, `${path}.id`, errors, 80);
|
|
5052
5206
|
if (id6 && !TIER.test(id6)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
5053
5207
|
if (seen.has(id6)) errors.push(`${path}.id duplicates "${id6}"`);
|
|
5054
5208
|
seen.add(id6);
|
|
5055
|
-
const name =
|
|
5209
|
+
const name = text5(item.name, `${path}.name`, errors, 160);
|
|
5056
5210
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
5057
5211
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
5058
5212
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -5064,7 +5218,7 @@ function tiersOf(value, errors) {
|
|
|
5064
5218
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
5065
5219
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
5066
5220
|
}
|
|
5067
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
5221
|
+
const blurb = item.blurb === void 0 ? void 0 : text5(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
5068
5222
|
return [{
|
|
5069
5223
|
id: id6,
|
|
5070
5224
|
name,
|
|
@@ -5120,7 +5274,7 @@ function policyOf(value, errors) {
|
|
|
5120
5274
|
const result = {};
|
|
5121
5275
|
for (const key of allowed2) {
|
|
5122
5276
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
5123
|
-
result[key] =
|
|
5277
|
+
result[key] = text5(input[key], `document.policy.${key}`, errors);
|
|
5124
5278
|
}
|
|
5125
5279
|
return result;
|
|
5126
5280
|
}
|
|
@@ -5158,9 +5312,9 @@ function validateSignupControlDocument(input) {
|
|
|
5158
5312
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
5159
5313
|
const source = recordOf(root.source);
|
|
5160
5314
|
const target = recordOf(root.target);
|
|
5161
|
-
const appId =
|
|
5162
|
-
const chapterId =
|
|
5163
|
-
const environment =
|
|
5315
|
+
const appId = text5(source?.appId, "document.source.appId", errors, 63);
|
|
5316
|
+
const chapterId = text5(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
5317
|
+
const environment = text5(target?.environment, "document.target.environment", errors, 63);
|
|
5164
5318
|
if (appId && !SLUG9.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
5165
5319
|
if (chapterId && !SLUG9.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
5166
5320
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
@@ -5204,7 +5358,7 @@ function validateSignupControlDocument(input) {
|
|
|
5204
5358
|
var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
|
|
5205
5359
|
var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
|
|
5206
5360
|
var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
|
|
5207
|
-
var
|
|
5361
|
+
var DIGEST2 = /^sha256:[0-9a-f]{64}$/;
|
|
5208
5362
|
var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
|
|
5209
5363
|
var MAX_TOKENS = 256;
|
|
5210
5364
|
var MAX_VALUE_LENGTH = 1e3;
|
|
@@ -5248,7 +5402,7 @@ function stateFor(revision, options) {
|
|
|
5248
5402
|
metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
|
|
5249
5403
|
};
|
|
5250
5404
|
}
|
|
5251
|
-
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !
|
|
5405
|
+
if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST2.test(revision.digest)) {
|
|
5252
5406
|
throw new TypeError("signup-control theme revision metadata is invalid");
|
|
5253
5407
|
}
|
|
5254
5408
|
return {
|
|
@@ -5337,9 +5491,9 @@ function markdownBenefits(lines, path, errors) {
|
|
|
5337
5491
|
errors.push(`${path} line ${index2 + 1} must be a Markdown bullet`);
|
|
5338
5492
|
continue;
|
|
5339
5493
|
}
|
|
5340
|
-
const
|
|
5341
|
-
if (
|
|
5342
|
-
if (
|
|
5494
|
+
const text6 = (match[1] ?? "").trim();
|
|
5495
|
+
if (text6.length > 2e3) errors.push(`${path} line ${index2 + 1} must be at most 2,000 characters`);
|
|
5496
|
+
if (text6) benefits.push(text6);
|
|
5343
5497
|
}
|
|
5344
5498
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
5345
5499
|
return benefits.slice(0, MAX_BENEFITS2);
|