@odla-ai/chapter 0.38.1 → 0.39.1
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 +49 -0
- package/dist/{chunk-SI3JHSEX.js → chunk-4K2Q5JBC.js} +660 -553
- package/dist/chunk-4K2Q5JBC.js.map +1 -0
- package/dist/{copy-context-Pbs3keLx.d.ts → copy-context-Buz_27Ed.d.ts} +26 -0
- package/dist/index.cjs +471 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +202 -3
- package/dist/index.d.ts +202 -3
- package/dist/index.js +471 -132
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +106 -3
- package/dist/ui/admin/index.js +5 -1
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +614 -111
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +127 -1
- package/dist/worker/index.d.ts +127 -1
- package/dist/worker/index.js +610 -107
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/greenfield.md +15 -0
- package/dist/chunk-SI3JHSEX.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,11 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ADMISSION_GRANTS_PATH: () => ADMISSION_GRANTS_PATH,
|
|
24
24
|
ADMISSION_GRANTS_SCHEMA_VERSION: () => ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
25
|
+
BUILDER_LIBRARY_ADMIN_PATH: () => BUILDER_LIBRARY_ADMIN_PATH,
|
|
26
|
+
BUILDER_LIBRARY_CONTROL_PATH: () => BUILDER_LIBRARY_CONTROL_PATH,
|
|
27
|
+
BUILDER_LIBRARY_MANIFEST_PATH: () => BUILDER_LIBRARY_MANIFEST_PATH,
|
|
28
|
+
BUILDER_LIBRARY_PROMOTE_PATH: () => BUILDER_LIBRARY_PROMOTE_PATH,
|
|
29
|
+
BUILDER_LIBRARY_SCHEMA_VERSION: () => BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
25
30
|
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION: () => CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
26
31
|
CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
|
|
27
32
|
DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
|
|
@@ -46,13 +51,17 @@ __export(index_exports, {
|
|
|
46
51
|
bookingDecision: () => bookingDecision,
|
|
47
52
|
brandTokens: () => brandTokens,
|
|
48
53
|
bucketSeries: () => bucketSeries,
|
|
54
|
+
buildBuilderLibraryManifest: () => buildBuilderLibraryManifest,
|
|
49
55
|
buildGroupSeed: () => buildGroupSeed,
|
|
50
56
|
buildTierSeeds: () => buildTierSeeds,
|
|
57
|
+
builderLibraryDigest: () => builderLibraryDigest,
|
|
51
58
|
canApprove: () => canApprove,
|
|
52
59
|
canBook: () => canBook,
|
|
53
60
|
canChangeRole: () => canChangeRole,
|
|
54
61
|
canTransition: () => canTransition,
|
|
62
|
+
canViewBuilderLibraryResource: () => canViewBuilderLibraryResource,
|
|
55
63
|
canceledPatch: () => canceledPatch,
|
|
64
|
+
canonicalBuilderLibrary: () => canonicalBuilderLibrary,
|
|
56
65
|
canonicalMemberContent: () => canonicalMemberContent,
|
|
57
66
|
canonicalSignupControl: () => canonicalSignupControl,
|
|
58
67
|
chapterBrandFromTokens: () => chapterBrandFromTokens,
|
|
@@ -68,6 +77,7 @@ __export(index_exports, {
|
|
|
68
77
|
clerkUserRequest: () => clerkUserRequest,
|
|
69
78
|
composeMemberContent: () => composeMemberContent,
|
|
70
79
|
createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
|
|
80
|
+
createBuilderLibraryEnvelope: () => createBuilderLibraryEnvelope,
|
|
71
81
|
createChapterIntegration: () => createChapterIntegration,
|
|
72
82
|
createClerkInvitation: () => createClerkInvitation,
|
|
73
83
|
createClerkUser: () => createClerkUser,
|
|
@@ -123,8 +133,10 @@ __export(index_exports, {
|
|
|
123
133
|
personInputFromApp: () => personInputFromApp,
|
|
124
134
|
planDelivery: () => planDelivery,
|
|
125
135
|
projectApplicant: () => projectApplicant,
|
|
136
|
+
projectBuilderLibraryDelivery: () => projectBuilderLibraryDelivery,
|
|
126
137
|
projectMemberContent: () => projectMemberContent,
|
|
127
138
|
projectSharedRecord: () => projectSharedRecord,
|
|
139
|
+
promoteBuilderLibraryResource: () => promoteBuilderLibraryResource,
|
|
128
140
|
publicGiftOffers: () => publicGiftOffers,
|
|
129
141
|
reconcileMeetings: () => reconcileMeetings,
|
|
130
142
|
refundedPatch: () => refundedPatch,
|
|
@@ -135,6 +147,7 @@ __export(index_exports, {
|
|
|
135
147
|
renewalPatch: () => renewalPatch,
|
|
136
148
|
resolveApplication: () => resolveApplication,
|
|
137
149
|
resolveAuth: () => resolveAuth,
|
|
150
|
+
resolveBuilderLibrary: () => resolveBuilderLibrary,
|
|
138
151
|
resolveChapterCopy: () => resolveChapterCopy,
|
|
139
152
|
resolveJoinContinuation: () => resolveJoinContinuation,
|
|
140
153
|
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
@@ -161,6 +174,7 @@ __export(index_exports, {
|
|
|
161
174
|
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
162
175
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
163
176
|
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
177
|
+
validateBuilderLibraryDocument: () => validateBuilderLibraryDocument,
|
|
164
178
|
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
165
179
|
validateMemberContentDocument: () => validateMemberContentDocument,
|
|
166
180
|
validateMembershipPageDocument: () => validateMembershipPageDocument,
|
|
@@ -644,6 +658,30 @@ var memberContentLocal = {
|
|
|
644
658
|
updatedBy: attr4("string", { optional: true })
|
|
645
659
|
}
|
|
646
660
|
};
|
|
661
|
+
var builderLibraryControls = {
|
|
662
|
+
attrs: {
|
|
663
|
+
id: id3(),
|
|
664
|
+
sourceId: attr4("string", { indexed: true }),
|
|
665
|
+
targetId: attr4("string", { indexed: true }),
|
|
666
|
+
environment: attr4("string", { indexed: true }),
|
|
667
|
+
revision: attr4("number", { indexed: true }),
|
|
668
|
+
digest: attr4("string", { indexed: true }),
|
|
669
|
+
document: attr4("json"),
|
|
670
|
+
active: attr4("boolean", { indexed: true }),
|
|
671
|
+
receivedAt: attr4("number", { indexed: true })
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
var builderLibraryLocal = {
|
|
675
|
+
attrs: {
|
|
676
|
+
id: id3(),
|
|
677
|
+
appId: attr4("string", { indexed: true }),
|
|
678
|
+
environment: attr4("string", { indexed: true }),
|
|
679
|
+
revision: attr4("number", { indexed: true }),
|
|
680
|
+
document: attr4("json"),
|
|
681
|
+
updatedAt: attr4("number", { indexed: true }),
|
|
682
|
+
updatedBy: attr4("string", { optional: true })
|
|
683
|
+
}
|
|
684
|
+
};
|
|
647
685
|
var groups = {
|
|
648
686
|
attrs: {
|
|
649
687
|
id: id3(),
|
|
@@ -715,7 +753,7 @@ var emailLog = {
|
|
|
715
753
|
sentAt: attr4("number", { indexed: true })
|
|
716
754
|
}
|
|
717
755
|
};
|
|
718
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
|
|
756
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false) {
|
|
719
757
|
const entities = {};
|
|
720
758
|
if (mode === "chapter") {
|
|
721
759
|
entities.applications = applicationsFor(application);
|
|
@@ -739,6 +777,10 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
739
777
|
if (auth.source === "table") entities.admins = admins;
|
|
740
778
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
741
779
|
if (includeNetworkNotes) entities.networkNotes = networkNotes;
|
|
780
|
+
if (includeBuilderLibrary) {
|
|
781
|
+
entities.builderLibraryControls = builderLibraryControls;
|
|
782
|
+
entities.builderLibraryLocal = builderLibraryLocal;
|
|
783
|
+
}
|
|
742
784
|
const schema = { entities, links: {} };
|
|
743
785
|
const rules = {};
|
|
744
786
|
for (const ns of Object.keys(entities)) {
|
|
@@ -1374,8 +1416,8 @@ var APPLICATION_LABELS = {
|
|
|
1374
1416
|
state: "State"
|
|
1375
1417
|
};
|
|
1376
1418
|
function humanize(id4) {
|
|
1377
|
-
const
|
|
1378
|
-
return
|
|
1419
|
+
const text4 = id4.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1420
|
+
return text4 ? `${text4[0].toUpperCase()}${text4.slice(1)}` : id4;
|
|
1379
1421
|
}
|
|
1380
1422
|
function applicationDefs(chapter) {
|
|
1381
1423
|
try {
|
|
@@ -1666,15 +1708,47 @@ function resolveMemberContent(input) {
|
|
|
1666
1708
|
};
|
|
1667
1709
|
}
|
|
1668
1710
|
|
|
1669
|
-
// src/config.ts
|
|
1711
|
+
// src/config-builder-library.ts
|
|
1670
1712
|
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1713
|
+
var SECRET_NAME3 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1714
|
+
function resolveBuilderLibrary(mode, input) {
|
|
1715
|
+
if (!input) return null;
|
|
1716
|
+
if (input.sources !== void 0 && !Array.isArray(input.sources)) {
|
|
1717
|
+
throw new Error("defineChapter.builderLibrary.sources: must be an array");
|
|
1718
|
+
}
|
|
1719
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1720
|
+
const sources = (input.sources ?? []).map((source, index) => {
|
|
1721
|
+
if (!source || typeof source !== "object" || !SLUG4.test(source.sourceId)) {
|
|
1722
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index}].sourceId: must be a lowercase app slug`);
|
|
1723
|
+
}
|
|
1724
|
+
if (source.sourceId === "built-not-found" && mode === "hub") {
|
|
1725
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index}]: a hub accepts Chapter sources, not itself`);
|
|
1726
|
+
}
|
|
1727
|
+
if (mode === "chapter" && source.sourceId !== "built-not-found") {
|
|
1728
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index}]: Chapters may accept only the built-not-found source`);
|
|
1729
|
+
}
|
|
1730
|
+
if (seen.has(source.sourceId)) {
|
|
1731
|
+
throw new Error(`defineChapter.builderLibrary.sources: duplicate source "${source.sourceId}"`);
|
|
1732
|
+
}
|
|
1733
|
+
seen.add(source.sourceId);
|
|
1734
|
+
const secretName = source.secretName?.trim() || "builder_library_control_secret";
|
|
1735
|
+
if (!SECRET_NAME3.test(secretName)) {
|
|
1736
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index}].secretName: must be a vault-key identifier`);
|
|
1737
|
+
}
|
|
1738
|
+
return { sourceId: source.sourceId, secretName };
|
|
1739
|
+
});
|
|
1740
|
+
return { enabled: true, localAuthoring: input.localAuthoring !== false, sources };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// src/config.ts
|
|
1744
|
+
var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1671
1745
|
function isResolvedCrm(x) {
|
|
1672
1746
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1673
1747
|
}
|
|
1674
1748
|
function defineChapter(config) {
|
|
1675
1749
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1676
1750
|
const { id: id4, name } = config;
|
|
1677
|
-
if (typeof id4 !== "string" || !
|
|
1751
|
+
if (typeof id4 !== "string" || !SLUG5.test(id4)) {
|
|
1678
1752
|
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1679
1753
|
}
|
|
1680
1754
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
@@ -1700,6 +1774,7 @@ function defineChapter(config) {
|
|
|
1700
1774
|
const network = resolveNetwork(config, crm);
|
|
1701
1775
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1702
1776
|
const memberContent = resolveMemberContent(config.memberContent);
|
|
1777
|
+
const builderLibrary = resolveBuilderLibrary(mode, config.builderLibrary);
|
|
1703
1778
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1704
1779
|
if (signupControl && mode !== "chapter") {
|
|
1705
1780
|
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
@@ -1713,7 +1788,8 @@ function defineChapter(config) {
|
|
|
1713
1788
|
network.targets.length > 0,
|
|
1714
1789
|
application,
|
|
1715
1790
|
Boolean(signupControl),
|
|
1716
|
-
Boolean(memberContent)
|
|
1791
|
+
Boolean(memberContent),
|
|
1792
|
+
Boolean(builderLibrary)
|
|
1717
1793
|
);
|
|
1718
1794
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1719
1795
|
const account = config.account ?? "none";
|
|
@@ -1771,6 +1847,7 @@ function defineChapter(config) {
|
|
|
1771
1847
|
network,
|
|
1772
1848
|
signupControl,
|
|
1773
1849
|
memberContent,
|
|
1850
|
+
builderLibrary,
|
|
1774
1851
|
formation,
|
|
1775
1852
|
crm,
|
|
1776
1853
|
auth,
|
|
@@ -2021,6 +2098,13 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2021
2098
|
...emails?.debugEmail ? { debugEmail: emails.debugEmail } : {}
|
|
2022
2099
|
});
|
|
2023
2100
|
const seeds = [...crmDesc.seeds ?? []];
|
|
2101
|
+
const builderLibrarySecrets = [...new Set(
|
|
2102
|
+
chapter.builderLibrary?.sources.map((source) => source.secretName) ?? []
|
|
2103
|
+
)].map((name) => ({
|
|
2104
|
+
name,
|
|
2105
|
+
description: "Signing secret for private Builder Library delivery from a configured source.",
|
|
2106
|
+
required: true
|
|
2107
|
+
}));
|
|
2024
2108
|
const group = chapter.groupSeed();
|
|
2025
2109
|
if (group) {
|
|
2026
2110
|
seeds.push({ id: "group", ns: "groups", key: { attr: "id", value: chapter.id }, attrs: { ...group, createdAt: now } });
|
|
@@ -2053,6 +2137,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2053
2137
|
},
|
|
2054
2138
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
2055
2139
|
seeds,
|
|
2140
|
+
secrets: builderLibrarySecrets,
|
|
2056
2141
|
probes: [
|
|
2057
2142
|
...crmDesc.probes ?? [],
|
|
2058
2143
|
// The snapshot route is deliberately private. A credential-free smoke
|
|
@@ -2069,6 +2154,11 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2069
2154
|
...chapter.memberContent ? [
|
|
2070
2155
|
{ path: "/api/member-content", expectedStatus: 401 },
|
|
2071
2156
|
{ path: "/api/admin/member-content", expectedStatus: 401 }
|
|
2157
|
+
] : [],
|
|
2158
|
+
...chapter.builderLibrary ? [
|
|
2159
|
+
{ path: "/api/builder-library-control", expectedStatus: 401 },
|
|
2160
|
+
{ path: "/api/admin/builder-library", expectedStatus: 401 },
|
|
2161
|
+
{ path: "/api/admin/builder-library/manifest", expectedStatus: 401 }
|
|
2072
2162
|
] : []
|
|
2073
2163
|
],
|
|
2074
2164
|
runbooks
|
|
@@ -2077,7 +2167,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2077
2167
|
|
|
2078
2168
|
// src/member-content-validation-helpers.ts
|
|
2079
2169
|
var ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2080
|
-
var
|
|
2170
|
+
var SLUG6 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2081
2171
|
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2082
2172
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2083
2173
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
@@ -2187,7 +2277,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2187
2277
|
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2188
2278
|
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2189
2279
|
if (id4 && !ID.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2190
|
-
if (slug && !
|
|
2280
|
+
if (slug && !SLUG6.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2191
2281
|
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2192
2282
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2193
2283
|
ids.add(id4);
|
|
@@ -2373,9 +2463,9 @@ function canonicalValue(value) {
|
|
|
2373
2463
|
}
|
|
2374
2464
|
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2375
2465
|
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2376
|
-
const
|
|
2377
|
-
const entries = Object.keys(
|
|
2378
|
-
const item =
|
|
2466
|
+
const record4 = value;
|
|
2467
|
+
const entries = Object.keys(record4).sort().flatMap((key) => {
|
|
2468
|
+
const item = record4[key];
|
|
2379
2469
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2380
2470
|
});
|
|
2381
2471
|
return `{${entries.join(",")}}`;
|
|
@@ -2391,6 +2481,243 @@ async function createMemberContentEnvelope(document) {
|
|
|
2391
2481
|
return { document, digest: await memberContentDigest(document) };
|
|
2392
2482
|
}
|
|
2393
2483
|
|
|
2484
|
+
// src/builder-library-validation.ts
|
|
2485
|
+
var ID2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2486
|
+
var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
2487
|
+
var MAX_RESOURCES = 200;
|
|
2488
|
+
var MAX_MARKDOWN2 = 65536;
|
|
2489
|
+
var MAX_TEXT2 = 500;
|
|
2490
|
+
var MAX_TAGS = 20;
|
|
2491
|
+
var KINDS = /* @__PURE__ */ new Set(["document", "note", "link", "inspiration", "image", "file"]);
|
|
2492
|
+
var STATUSES = /* @__PURE__ */ new Set(["draft", "published", "archived"]);
|
|
2493
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "resources"]);
|
|
2494
|
+
var SOURCE_KEYS2 = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2495
|
+
var TARGET_KEYS2 = /* @__PURE__ */ new Set(["appId", "environment"]);
|
|
2496
|
+
var RESOURCE_KEYS = /* @__PURE__ */ new Set(["id", "kind", "title", "summary", "tags", "scope", "status", "revision", "bodyMarkdown", "url", "brandAsset", "provenance"]);
|
|
2497
|
+
var SCOPE_KEYS = /* @__PURE__ */ new Set(["kind", "chapterId"]);
|
|
2498
|
+
var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
|
|
2499
|
+
var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
|
|
2500
|
+
var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
|
|
2501
|
+
var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2502
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
2503
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2504
|
+
}
|
|
2505
|
+
function text(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
2506
|
+
if (value === void 0 && !required) return void 0;
|
|
2507
|
+
if (typeof value !== "string" || required && !value.trim()) {
|
|
2508
|
+
errors.push(`${path} must be a non-empty string`);
|
|
2509
|
+
return void 0;
|
|
2510
|
+
}
|
|
2511
|
+
const result = value.trim();
|
|
2512
|
+
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2513
|
+
return result;
|
|
2514
|
+
}
|
|
2515
|
+
function integer(value, path, errors, minimum = 1) {
|
|
2516
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
2517
|
+
return Number(value);
|
|
2518
|
+
}
|
|
2519
|
+
function scopeValue(value, path, errors) {
|
|
2520
|
+
const raw = record2(value);
|
|
2521
|
+
if (!raw) {
|
|
2522
|
+
errors.push(`${path} must be an object`);
|
|
2523
|
+
return null;
|
|
2524
|
+
}
|
|
2525
|
+
unknownKeys2(raw, SCOPE_KEYS, path, errors);
|
|
2526
|
+
if (raw.kind === "global" || raw.kind === "built_not_found") {
|
|
2527
|
+
if (raw.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
2528
|
+
return { kind: raw.kind };
|
|
2529
|
+
}
|
|
2530
|
+
if (raw.kind === "chapter") {
|
|
2531
|
+
const chapterId = text(raw.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
2532
|
+
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
2533
|
+
return { kind: "chapter", chapterId };
|
|
2534
|
+
}
|
|
2535
|
+
errors.push(`${path}.kind must be chapter, built_not_found, or global`);
|
|
2536
|
+
return null;
|
|
2537
|
+
}
|
|
2538
|
+
function resourceValue(value, path, errors) {
|
|
2539
|
+
const raw = record2(value);
|
|
2540
|
+
if (!raw) {
|
|
2541
|
+
errors.push(`${path} must be an object`);
|
|
2542
|
+
return null;
|
|
2543
|
+
}
|
|
2544
|
+
unknownKeys2(raw, RESOURCE_KEYS, path, errors);
|
|
2545
|
+
const id4 = text(raw.id, `${path}.id`, errors, 80) ?? "";
|
|
2546
|
+
if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
2547
|
+
const kind = text(raw.kind, `${path}.kind`, errors) ?? "";
|
|
2548
|
+
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
2549
|
+
const title = text(raw.title, `${path}.title`, errors, 240) ?? "";
|
|
2550
|
+
const summary = text(raw.summary, `${path}.summary`, errors, 1e3, false);
|
|
2551
|
+
const status = text(raw.status, `${path}.status`, errors) ?? "";
|
|
2552
|
+
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
2553
|
+
const revision = integer(raw.revision, `${path}.revision`, errors);
|
|
2554
|
+
const scope = scopeValue(raw.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
2555
|
+
const tags = Array.isArray(raw.tags) ? raw.tags.map((tag, index) => text(tag, `${path}.tags[${index}]`, errors, 80) ?? "") : [];
|
|
2556
|
+
if (!Array.isArray(raw.tags)) errors.push(`${path}.tags must be an array`);
|
|
2557
|
+
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
2558
|
+
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
2559
|
+
const bodyMarkdown = text(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
|
|
2560
|
+
const url = text(raw.url, `${path}.url`, errors, 2048, false);
|
|
2561
|
+
if (url) {
|
|
2562
|
+
try {
|
|
2563
|
+
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
2564
|
+
} catch {
|
|
2565
|
+
errors.push(`${path}.url must be a valid https URL`);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
const asset = raw.brandAsset === void 0 ? null : record2(raw.brandAsset);
|
|
2569
|
+
if (raw.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
2570
|
+
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
2571
|
+
const brandAsset = asset ? {
|
|
2572
|
+
bookId: text(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
2573
|
+
assetId: text(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
2574
|
+
...asset.filename === void 0 ? {} : { filename: text(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
2575
|
+
} : void 0;
|
|
2576
|
+
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
2577
|
+
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
2578
|
+
if ((kind === "image" || kind === "file") && !brandAsset) errors.push(`${path}.brandAsset is required for ${kind}`);
|
|
2579
|
+
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
2580
|
+
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
2581
|
+
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
2582
|
+
const provenanceRaw = record2(raw.provenance);
|
|
2583
|
+
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
2584
|
+
if (provenanceRaw) unknownKeys2(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
2585
|
+
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record2(provenanceRaw.promotedFrom);
|
|
2586
|
+
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
2587
|
+
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
2588
|
+
const promotedFrom = promotedRaw ? {
|
|
2589
|
+
chapterId: text(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
2590
|
+
resourceId: text(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
2591
|
+
resourceRevision: integer(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
2592
|
+
documentRevision: integer(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
2593
|
+
promotedBy: text(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
2594
|
+
promotedAt: integer(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
2595
|
+
} : void 0;
|
|
2596
|
+
const provenance = {
|
|
2597
|
+
createdBy: text(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
2598
|
+
createdAt: integer(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
2599
|
+
updatedBy: text(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
2600
|
+
updatedAt: integer(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
2601
|
+
...promotedFrom ? { promotedFrom } : {}
|
|
2602
|
+
};
|
|
2603
|
+
return { id: id4, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
2604
|
+
}
|
|
2605
|
+
function validateBuilderLibraryDocument(value, options = {}) {
|
|
2606
|
+
const errors = [];
|
|
2607
|
+
const raw = record2(value);
|
|
2608
|
+
if (!raw) return { ok: false, errors: ["document must be an object"] };
|
|
2609
|
+
unknownKeys2(raw, ROOT_KEYS2, "document", errors);
|
|
2610
|
+
if (raw.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2611
|
+
const revision = integer(raw.revision, "document.revision", errors);
|
|
2612
|
+
const generatedAt = integer(raw.generatedAt, "document.generatedAt", errors, 0);
|
|
2613
|
+
const sourceRaw = record2(raw.source);
|
|
2614
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2615
|
+
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
2616
|
+
const sourceType = sourceRaw?.type;
|
|
2617
|
+
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
2618
|
+
const sourceAppId = text(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
2619
|
+
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2620
|
+
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2621
|
+
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2622
|
+
const targetRaw = raw.target === void 0 ? null : record2(raw.target);
|
|
2623
|
+
if (raw.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
2624
|
+
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
2625
|
+
const target = targetRaw ? {
|
|
2626
|
+
appId: text(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
2627
|
+
environment: text(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
2628
|
+
} : void 0;
|
|
2629
|
+
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
2630
|
+
if (!Array.isArray(raw.resources)) errors.push("document.resources must be an array");
|
|
2631
|
+
const inputs = Array.isArray(raw.resources) ? raw.resources : [];
|
|
2632
|
+
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
2633
|
+
const resources = inputs.map((item, index) => resourceValue(item, `document.resources[${index}]`, errors)).filter(Boolean);
|
|
2634
|
+
const ids = resources.map(({ id: id4 }) => id4);
|
|
2635
|
+
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
2636
|
+
for (const resource of resources) {
|
|
2637
|
+
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
2638
|
+
errors.push(`document.resources.${resource.id}: Chapter sources may author only their own chapter scope`);
|
|
2639
|
+
}
|
|
2640
|
+
if (sourceType === "built_not_found" && resource.scope.kind === "chapter") {
|
|
2641
|
+
errors.push(`document.resources.${resource.id}: Built Not Found sources may author only built_not_found or global scope`);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
if (errors.length) return { ok: false, errors };
|
|
2645
|
+
return { ok: true, value: { schemaVersion: 1, revision, generatedAt, source: { type: sourceType, appId: sourceAppId }, ...target ? { target } : {}, resources } };
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// src/builder-library.ts
|
|
2649
|
+
var BUILDER_LIBRARY_SCHEMA_VERSION = 1;
|
|
2650
|
+
var BUILDER_LIBRARY_ADMIN_PATH = "/api/admin/builder-library";
|
|
2651
|
+
var BUILDER_LIBRARY_MANIFEST_PATH = "/api/admin/builder-library/manifest";
|
|
2652
|
+
var BUILDER_LIBRARY_PROMOTE_PATH = "/api/admin/builder-library/promote";
|
|
2653
|
+
var BUILDER_LIBRARY_CONTROL_PATH = "/api/builder-library-control";
|
|
2654
|
+
function canViewBuilderLibraryResource(resource, viewer) {
|
|
2655
|
+
if (viewer.kind === "built_not_found") return true;
|
|
2656
|
+
return resource.scope.kind === "global" || resource.scope.kind === "chapter" && resource.scope.chapterId === viewer.chapterId;
|
|
2657
|
+
}
|
|
2658
|
+
function buildBuilderLibraryManifest(documents, viewer, generatedAt = Date.now(), limit = 100) {
|
|
2659
|
+
const ordered = documents.flatMap((document) => document.resources.map((resource) => ({ document, resource }))).filter(({ resource }) => resource.status === "published" && canViewBuilderLibraryResource(resource, viewer)).sort((a, b) => a.resource.title.localeCompare(b.resource.title) || a.document.source.appId.localeCompare(b.document.source.appId) || a.resource.id.localeCompare(b.resource.id));
|
|
2660
|
+
return {
|
|
2661
|
+
schemaVersion: BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
2662
|
+
generatedAt,
|
|
2663
|
+
viewer,
|
|
2664
|
+
revisions: documents.map(({ source, revision }) => ({ source, revision })),
|
|
2665
|
+
resources: ordered.slice(0, limit).map(({ resource }) => resource),
|
|
2666
|
+
truncated: ordered.length > limit
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
function projectBuilderLibraryDelivery(document, target) {
|
|
2670
|
+
return {
|
|
2671
|
+
...document,
|
|
2672
|
+
target,
|
|
2673
|
+
resources: document.source.type === "built_not_found" ? document.resources.filter((resource) => resource.scope.kind === "global") : document.resources
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
function promoteBuilderLibraryResource(input) {
|
|
2677
|
+
if (input.resource.scope.kind !== "chapter") throw new TypeError("only a Chapter-scoped resource can be promoted");
|
|
2678
|
+
const chapterId = input.resource.scope.chapterId;
|
|
2679
|
+
return {
|
|
2680
|
+
...input.resource,
|
|
2681
|
+
id: input.globalId ?? `${chapterId}-${input.resource.id}`,
|
|
2682
|
+
scope: { kind: "global" },
|
|
2683
|
+
revision: 1,
|
|
2684
|
+
provenance: {
|
|
2685
|
+
createdBy: input.actor,
|
|
2686
|
+
createdAt: input.promotedAt,
|
|
2687
|
+
updatedBy: input.actor,
|
|
2688
|
+
updatedAt: input.promotedAt,
|
|
2689
|
+
promotedFrom: {
|
|
2690
|
+
chapterId,
|
|
2691
|
+
resourceId: input.resource.id,
|
|
2692
|
+
resourceRevision: input.resource.revision,
|
|
2693
|
+
documentRevision: input.chapterDocumentRevision,
|
|
2694
|
+
promotedBy: input.actor,
|
|
2695
|
+
promotedAt: input.promotedAt
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// src/builder-library-canonical.ts
|
|
2702
|
+
var encoder2 = new TextEncoder();
|
|
2703
|
+
function canonicalValue2(value) {
|
|
2704
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") return JSON.stringify(value);
|
|
2705
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2706
|
+
if (typeof value !== "object") throw new TypeError("Builder Library contains an unsupported value");
|
|
2707
|
+
const record4 = value;
|
|
2708
|
+
return `{${Object.keys(record4).sort().flatMap((key) => record4[key] === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(record4[key])}`]).join(",")}}`;
|
|
2709
|
+
}
|
|
2710
|
+
function canonicalBuilderLibrary(value) {
|
|
2711
|
+
return canonicalValue2(value);
|
|
2712
|
+
}
|
|
2713
|
+
async function builderLibraryDigest(document) {
|
|
2714
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalBuilderLibrary(document)));
|
|
2715
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2716
|
+
}
|
|
2717
|
+
async function createBuilderLibraryEnvelope(document) {
|
|
2718
|
+
return { document, digest: await builderLibraryDigest(document) };
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2394
2721
|
// src/email.ts
|
|
2395
2722
|
function render(template, vars) {
|
|
2396
2723
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -2421,10 +2748,10 @@ function planDelivery(input) {
|
|
|
2421
2748
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
2422
2749
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
2423
2750
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
2424
|
-
const
|
|
2751
|
+
const text4 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
2425
2752
|
|
|
2426
2753
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
2427
|
-
return { deliver: true, transport, to, subject, text:
|
|
2754
|
+
return { deliver: true, transport, to, subject, text: text4, redirected: redirect };
|
|
2428
2755
|
}
|
|
2429
2756
|
|
|
2430
2757
|
// src/notify.ts
|
|
@@ -2507,9 +2834,21 @@ function webhookMutationId(eventId) {
|
|
|
2507
2834
|
function findApplicationRef(obj) {
|
|
2508
2835
|
const metaOf = (v) => v && typeof v === "object" ? v.metadata ?? {} : {};
|
|
2509
2836
|
const pick = (m) => typeof m.applicationId === "string" ? m.applicationId : void 0;
|
|
2510
|
-
const
|
|
2837
|
+
const candidates = [
|
|
2838
|
+
metaOf(obj),
|
|
2839
|
+
metaOf(obj.subscription_details),
|
|
2840
|
+
metaOf(obj.parent?.subscription_details)
|
|
2841
|
+
];
|
|
2842
|
+
const applicationId = candidates.map(pick).find(Boolean);
|
|
2843
|
+
const appId = candidates.map((metadata) => typeof metadata.odlaAppId === "string" ? metadata.odlaAppId : void 0).find(Boolean);
|
|
2844
|
+
const environment = candidates.map((metadata) => typeof metadata.odlaEnvironment === "string" ? metadata.odlaEnvironment : void 0).find(Boolean);
|
|
2511
2845
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
2512
|
-
return {
|
|
2846
|
+
return {
|
|
2847
|
+
...applicationId ? { applicationId } : {},
|
|
2848
|
+
...customerId ? { customerId } : {},
|
|
2849
|
+
...appId ? { appId } : {},
|
|
2850
|
+
...environment ? { environment } : {}
|
|
2851
|
+
};
|
|
2513
2852
|
}
|
|
2514
2853
|
function findGiftRef(obj) {
|
|
2515
2854
|
const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
|
|
@@ -2569,25 +2908,25 @@ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
|
2569
2908
|
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
2570
2909
|
|
|
2571
2910
|
// src/signup-control-canonical.ts
|
|
2572
|
-
var
|
|
2573
|
-
function
|
|
2911
|
+
var encoder3 = new TextEncoder();
|
|
2912
|
+
function canonicalValue3(value) {
|
|
2574
2913
|
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2575
2914
|
return JSON.stringify(value);
|
|
2576
2915
|
}
|
|
2577
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2916
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue3).join(",")}]`;
|
|
2578
2917
|
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2579
|
-
const
|
|
2580
|
-
const entries = Object.keys(
|
|
2581
|
-
const item =
|
|
2582
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${
|
|
2918
|
+
const record4 = value;
|
|
2919
|
+
const entries = Object.keys(record4).sort().flatMap((key) => {
|
|
2920
|
+
const item = record4[key];
|
|
2921
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue3(item)}`];
|
|
2583
2922
|
});
|
|
2584
2923
|
return `{${entries.join(",")}}`;
|
|
2585
2924
|
}
|
|
2586
2925
|
function canonicalSignupControl(value) {
|
|
2587
|
-
return
|
|
2926
|
+
return canonicalValue3(value);
|
|
2588
2927
|
}
|
|
2589
2928
|
async function signupControlDigest(document) {
|
|
2590
|
-
const digest = await crypto.subtle.digest("SHA-256",
|
|
2929
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder3.encode(canonicalSignupControl(document)));
|
|
2591
2930
|
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2592
2931
|
}
|
|
2593
2932
|
async function createSignupControlEnvelope(document) {
|
|
@@ -2595,7 +2934,7 @@ async function createSignupControlEnvelope(document) {
|
|
|
2595
2934
|
}
|
|
2596
2935
|
|
|
2597
2936
|
// src/admission-grants.ts
|
|
2598
|
-
var
|
|
2937
|
+
var ID3 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
2599
2938
|
var MAX_GRANTS = 2e3;
|
|
2600
2939
|
var MAX_NOTE = 2e3;
|
|
2601
2940
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -2617,10 +2956,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
2617
2956
|
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
2618
2957
|
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
2619
2958
|
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
2620
|
-
if (!
|
|
2959
|
+
if (!ID3.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
2621
2960
|
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
2622
|
-
if (!
|
|
2623
|
-
if (!
|
|
2961
|
+
if (!ID3.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
2962
|
+
if (!ID3.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
2624
2963
|
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
2625
2964
|
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
2626
2965
|
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
@@ -2635,7 +2974,7 @@ function validateAdmissionGrantDocument(input) {
|
|
|
2635
2974
|
const decision = grant.decision;
|
|
2636
2975
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
2637
2976
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
2638
|
-
if (!
|
|
2977
|
+
if (!ID3.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
2639
2978
|
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
2640
2979
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
2641
2980
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
@@ -2792,11 +3131,11 @@ function resolveJoinContinuation(input) {
|
|
|
2792
3131
|
// src/gift-offers.ts
|
|
2793
3132
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2794
3133
|
var MAX_GIFT_OFFERS = 20;
|
|
2795
|
-
var
|
|
3134
|
+
var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2796
3135
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2797
3136
|
var CURRENCY = /^[a-z]{3}$/;
|
|
2798
|
-
var
|
|
2799
|
-
var
|
|
3137
|
+
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3138
|
+
var text2 = (value, path, errors, max) => {
|
|
2800
3139
|
if (typeof value !== "string" || !value.trim()) {
|
|
2801
3140
|
errors.push(`${path} is required`);
|
|
2802
3141
|
return "";
|
|
@@ -2806,7 +3145,7 @@ var text = (value, path, errors, max) => {
|
|
|
2806
3145
|
};
|
|
2807
3146
|
function validateGiftOfferCatalog(input, tierIds) {
|
|
2808
3147
|
const errors = [];
|
|
2809
|
-
const root =
|
|
3148
|
+
const root = record3(input);
|
|
2810
3149
|
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2811
3150
|
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2812
3151
|
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
@@ -2817,30 +3156,30 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2817
3156
|
const offers = [];
|
|
2818
3157
|
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2819
3158
|
const path = `document.giftOffers.offers[${index}]`;
|
|
2820
|
-
const item =
|
|
3159
|
+
const item = record3(raw);
|
|
2821
3160
|
if (!item) {
|
|
2822
3161
|
errors.push(`${path} must be an object`);
|
|
2823
3162
|
continue;
|
|
2824
3163
|
}
|
|
2825
3164
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2826
3165
|
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2827
|
-
const id4 =
|
|
2828
|
-
if (id4 && !
|
|
3166
|
+
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
3167
|
+
if (id4 && !ID4.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2829
3168
|
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2830
3169
|
ids.add(id4);
|
|
2831
|
-
const targetRaw =
|
|
3170
|
+
const targetRaw = record3(item.target);
|
|
2832
3171
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2833
3172
|
if (targetRaw?.type === "tier") {
|
|
2834
|
-
const tierId =
|
|
3173
|
+
const tierId = text2(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
2835
3174
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
2836
3175
|
target = { type: "tier", tierId };
|
|
2837
3176
|
} else if (targetRaw?.type === "entitlement") {
|
|
2838
|
-
const entitlementKey =
|
|
2839
|
-
if (entitlementKey && !
|
|
3177
|
+
const entitlementKey = text2(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
3178
|
+
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2840
3179
|
target = { type: "entitlement", entitlementKey };
|
|
2841
3180
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2842
3181
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
2843
|
-
const stripePriceId =
|
|
3182
|
+
const stripePriceId = text2(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
2844
3183
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2845
3184
|
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`);
|
|
2846
3185
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -2862,9 +3201,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2862
3201
|
redemptionLifetimeHours: lifetime,
|
|
2863
3202
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
2864
3203
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
2865
|
-
title:
|
|
2866
|
-
description:
|
|
2867
|
-
ctaLabel:
|
|
3204
|
+
title: text2(item.title, `${path}.title`, errors, 160),
|
|
3205
|
+
description: text2(item.description, `${path}.description`, errors, 2e3),
|
|
3206
|
+
ctaLabel: text2(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
2868
3207
|
});
|
|
2869
3208
|
}
|
|
2870
3209
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -2916,34 +3255,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
2916
3255
|
if (readable && raw.length <= 64) return raw;
|
|
2917
3256
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
2918
3257
|
}
|
|
2919
|
-
function normalizeSharedRecord(
|
|
2920
|
-
if ("input" in
|
|
3258
|
+
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
3259
|
+
if ("input" in record4 && record4.version === 2) {
|
|
2921
3260
|
return {
|
|
2922
3261
|
version: 2,
|
|
2923
|
-
sourceId:
|
|
2924
|
-
sourceRecordId:
|
|
2925
|
-
type:
|
|
2926
|
-
input:
|
|
3262
|
+
sourceId: record4.source.siteId,
|
|
3263
|
+
sourceRecordId: record4.source.recordId,
|
|
3264
|
+
type: record4.type,
|
|
3265
|
+
input: record4.input
|
|
2927
3266
|
};
|
|
2928
3267
|
}
|
|
2929
|
-
if ("input" in
|
|
3268
|
+
if ("input" in record4) {
|
|
2930
3269
|
return {
|
|
2931
3270
|
version: 1,
|
|
2932
3271
|
sourceId: fallbackSourceId,
|
|
2933
|
-
sourceRecordId:
|
|
2934
|
-
type:
|
|
2935
|
-
input:
|
|
3272
|
+
sourceRecordId: record4.hubRecordId,
|
|
3273
|
+
type: record4.type,
|
|
3274
|
+
input: record4.input
|
|
2936
3275
|
};
|
|
2937
3276
|
}
|
|
2938
|
-
if ("type" in
|
|
2939
|
-
const input = { name:
|
|
3277
|
+
if ("type" in record4 && record4.type === "company") {
|
|
3278
|
+
const input = { name: record4.name };
|
|
2940
3279
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
2941
|
-
if (
|
|
3280
|
+
if (record4[key]) input[key] = record4[key];
|
|
2942
3281
|
}
|
|
2943
3282
|
return {
|
|
2944
3283
|
version: 1,
|
|
2945
3284
|
sourceId: fallbackSourceId,
|
|
2946
|
-
sourceRecordId:
|
|
3285
|
+
sourceRecordId: record4.hubRecordId,
|
|
2947
3286
|
type: "company",
|
|
2948
3287
|
input
|
|
2949
3288
|
};
|
|
@@ -2951,28 +3290,28 @@ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
|
2951
3290
|
return {
|
|
2952
3291
|
version: 1,
|
|
2953
3292
|
sourceId: fallbackSourceId,
|
|
2954
|
-
sourceRecordId:
|
|
3293
|
+
sourceRecordId: record4.hubRecordId,
|
|
2955
3294
|
type: "person",
|
|
2956
|
-
input: sharedPersonInput(
|
|
3295
|
+
input: sharedPersonInput(record4)
|
|
2957
3296
|
};
|
|
2958
3297
|
}
|
|
2959
|
-
function sharedRecordFromCrm(crm,
|
|
2960
|
-
if (target.fields && !target.fields[
|
|
2961
|
-
throw new Error(`${target.name} does not accept "${
|
|
3298
|
+
function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
3299
|
+
if (target.fields && !target.fields[record4.type]) {
|
|
3300
|
+
throw new Error(`${target.name} does not accept "${record4.type}" records`);
|
|
2962
3301
|
}
|
|
2963
|
-
const def = crm.type(
|
|
2964
|
-
const fields = target.fields?.[
|
|
3302
|
+
const def = crm.type(record4.type);
|
|
3303
|
+
const fields = target.fields?.[record4.type] ?? DEFAULT_SHARE_FIELDS[record4.type];
|
|
2965
3304
|
if (!fields) {
|
|
2966
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
3305
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record4.type}" records`);
|
|
2967
3306
|
}
|
|
2968
3307
|
const nameField = def.nameField ?? "name";
|
|
2969
3308
|
const input = {};
|
|
2970
3309
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
2971
|
-
const value =
|
|
3310
|
+
const value = record4.fields?.[field];
|
|
2972
3311
|
if (value !== void 0) input[field] = value;
|
|
2973
3312
|
}
|
|
2974
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
2975
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
3313
|
+
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
3314
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
2976
3315
|
}
|
|
2977
3316
|
async function upsertPerson(deps, opts) {
|
|
2978
3317
|
const email = opts.email.toLowerCase();
|
|
@@ -2986,11 +3325,11 @@ async function upsertPerson(deps, opts) {
|
|
|
2986
3325
|
const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
2987
3326
|
return { recordId: created.id };
|
|
2988
3327
|
}
|
|
2989
|
-
async function findSharedRecord(deps,
|
|
3328
|
+
async function findSharedRecord(deps, record4, tag) {
|
|
2990
3329
|
const structured = await (0, import_crm5.getRecordByOrigin)(
|
|
2991
3330
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
2992
|
-
|
|
2993
|
-
|
|
3331
|
+
record4.sourceId,
|
|
3332
|
+
record4.sourceRecordId
|
|
2994
3333
|
);
|
|
2995
3334
|
if (structured) return structured;
|
|
2996
3335
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -2999,53 +3338,53 @@ async function findSharedRecord(deps, record3, tag) {
|
|
|
2999
3338
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
3000
3339
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
3001
3340
|
}
|
|
3002
|
-
const def = deps.crm.type(
|
|
3341
|
+
const def = deps.crm.type(record4.type);
|
|
3003
3342
|
const emailField = def.emailField;
|
|
3004
|
-
if (emailField && typeof
|
|
3005
|
-
const primaryEmail =
|
|
3343
|
+
if (emailField && typeof record4.input[emailField] === "string") {
|
|
3344
|
+
const primaryEmail = record4.input[emailField].toLowerCase();
|
|
3006
3345
|
const found = await deps.db.query({
|
|
3007
|
-
crm_record: { $: { where: { type:
|
|
3346
|
+
crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
|
|
3008
3347
|
});
|
|
3009
3348
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
3010
3349
|
}
|
|
3011
|
-
const domain =
|
|
3350
|
+
const domain = record4.input.domain;
|
|
3012
3351
|
const domainSlot = def.fields.domain?.slot;
|
|
3013
3352
|
if (typeof domain === "string" && domainSlot) {
|
|
3014
3353
|
const found = await deps.db.query({
|
|
3015
|
-
crm_record: { $: { where: { type:
|
|
3354
|
+
crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
|
|
3016
3355
|
});
|
|
3017
3356
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
3018
3357
|
}
|
|
3019
3358
|
const nameField = def.nameField ?? "name";
|
|
3020
|
-
const name =
|
|
3021
|
-
if (
|
|
3359
|
+
const name = record4.input[nameField];
|
|
3360
|
+
if (record4.type === "company" && typeof name === "string" && name.trim()) {
|
|
3022
3361
|
const found = await deps.db.query({
|
|
3023
|
-
crm_record: { $: { where: { type:
|
|
3362
|
+
crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
|
|
3024
3363
|
});
|
|
3025
3364
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
3026
3365
|
}
|
|
3027
3366
|
return void 0;
|
|
3028
3367
|
}
|
|
3029
3368
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
3030
|
-
const
|
|
3031
|
-
if (!
|
|
3369
|
+
const record4 = normalizeSharedRecord(shared, options.sourceId);
|
|
3370
|
+
if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
|
|
3032
3371
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
3033
3372
|
}
|
|
3034
|
-
if (options.sourceId && options.sourceId !==
|
|
3373
|
+
if (options.sourceId && options.sourceId !== record4.sourceId) {
|
|
3035
3374
|
throw new Error("signed sender does not match payload source");
|
|
3036
3375
|
}
|
|
3037
|
-
const tag = networkSourceTag(
|
|
3376
|
+
const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
|
|
3038
3377
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
3039
|
-
const existing = await findSharedRecord(deps,
|
|
3378
|
+
const existing = await findSharedRecord(deps, record4, tag);
|
|
3040
3379
|
let recordId;
|
|
3041
3380
|
if (existing && typeof existing.id === "string") {
|
|
3042
|
-
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input:
|
|
3381
|
+
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record4.input });
|
|
3043
3382
|
recordId = existing.id;
|
|
3044
3383
|
} else {
|
|
3045
|
-
recordId = `network_${shortHash(`${
|
|
3384
|
+
recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
|
|
3046
3385
|
await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
3047
|
-
type:
|
|
3048
|
-
input:
|
|
3386
|
+
type: record4.type,
|
|
3387
|
+
input: record4.input,
|
|
3049
3388
|
mutationId: `share-create:${tag}`
|
|
3050
3389
|
});
|
|
3051
3390
|
}
|
|
@@ -3055,10 +3394,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
3055
3394
|
);
|
|
3056
3395
|
await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
|
|
3057
3396
|
recordId,
|
|
3058
|
-
sourceId:
|
|
3059
|
-
sourceRecordId:
|
|
3397
|
+
sourceId: record4.sourceId,
|
|
3398
|
+
sourceRecordId: record4.sourceRecordId,
|
|
3060
3399
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
3061
|
-
payloadVersion:
|
|
3400
|
+
payloadVersion: record4.version
|
|
3062
3401
|
});
|
|
3063
3402
|
return { recordId };
|
|
3064
3403
|
}
|
|
@@ -3342,8 +3681,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
3342
3681
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
3343
3682
|
const page = Array.isArray(data) ? data : [];
|
|
3344
3683
|
for (const u of page) {
|
|
3345
|
-
const
|
|
3346
|
-
if (
|
|
3684
|
+
const record4 = toRecord(u);
|
|
3685
|
+
if (record4) out.push(record4);
|
|
3347
3686
|
}
|
|
3348
3687
|
if (page.length < PAGE) break;
|
|
3349
3688
|
}
|
|
@@ -3750,7 +4089,7 @@ var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
|
3750
4089
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
3751
4090
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3752
4091
|
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
3753
|
-
var
|
|
4092
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
3754
4093
|
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
3755
4094
|
"tierId",
|
|
3756
4095
|
"variant",
|
|
@@ -3769,7 +4108,7 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
|
3769
4108
|
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
3770
4109
|
var MAX_TIERS = 20;
|
|
3771
4110
|
var MAX_BENEFITS = 24;
|
|
3772
|
-
var
|
|
4111
|
+
var MAX_MARKDOWN3 = 65536;
|
|
3773
4112
|
var MAX_LABEL = 160;
|
|
3774
4113
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3775
4114
|
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
@@ -3786,16 +4125,16 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
3786
4125
|
errors.push(`${path} must be a string`);
|
|
3787
4126
|
return void 0;
|
|
3788
4127
|
}
|
|
3789
|
-
const
|
|
3790
|
-
if (required && !
|
|
3791
|
-
if (
|
|
3792
|
-
return
|
|
4128
|
+
const text4 = value.trim();
|
|
4129
|
+
if (required && !text4) errors.push(`${path} is required`);
|
|
4130
|
+
if (text4.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
4131
|
+
return text4;
|
|
3793
4132
|
}
|
|
3794
4133
|
function validateMembershipPageDocument(input, options = {}) {
|
|
3795
4134
|
const errors = [];
|
|
3796
4135
|
const root = objectRecord(input);
|
|
3797
4136
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3798
|
-
rejectUnknownKeys(root,
|
|
4137
|
+
rejectUnknownKeys(root, ROOT_KEYS3, "document", errors);
|
|
3799
4138
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3800
4139
|
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
3801
4140
|
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
@@ -3838,8 +4177,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3838
4177
|
else {
|
|
3839
4178
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
3840
4179
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
3841
|
-
const
|
|
3842
|
-
return
|
|
4180
|
+
const text4 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
4181
|
+
return text4 ? [text4] : [];
|
|
3843
4182
|
});
|
|
3844
4183
|
}
|
|
3845
4184
|
}
|
|
@@ -3849,7 +4188,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3849
4188
|
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
3850
4189
|
else {
|
|
3851
4190
|
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
3852
|
-
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors,
|
|
4191
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN3, true);
|
|
3853
4192
|
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
3854
4193
|
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
3855
4194
|
}
|
|
@@ -3871,10 +4210,10 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3871
4210
|
}
|
|
3872
4211
|
}
|
|
3873
4212
|
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
3874
|
-
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors,
|
|
4213
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN3);
|
|
3875
4214
|
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
3876
4215
|
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
3877
|
-
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors,
|
|
4216
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN3);
|
|
3878
4217
|
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
3879
4218
|
tiers2.push({
|
|
3880
4219
|
tierId,
|
|
@@ -3896,7 +4235,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3896
4235
|
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
3897
4236
|
}
|
|
3898
4237
|
}
|
|
3899
|
-
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors,
|
|
4238
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN3);
|
|
3900
4239
|
if (errors.length) return { ok: false, errors };
|
|
3901
4240
|
return {
|
|
3902
4241
|
ok: true,
|
|
@@ -3915,11 +4254,11 @@ function membershipPageDocument(input, options = {}) {
|
|
|
3915
4254
|
}
|
|
3916
4255
|
|
|
3917
4256
|
// src/signup-control-validate.ts
|
|
3918
|
-
var
|
|
4257
|
+
var SLUG7 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3919
4258
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
3920
4259
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3921
4260
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
3922
|
-
var
|
|
4261
|
+
var ROOT_KEYS4 = /* @__PURE__ */ new Set([
|
|
3923
4262
|
"schemaVersion",
|
|
3924
4263
|
"revision",
|
|
3925
4264
|
"generatedAt",
|
|
@@ -3935,10 +4274,10 @@ var ROOT_KEYS3 = /* @__PURE__ */ new Set([
|
|
|
3935
4274
|
]);
|
|
3936
4275
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
3937
4276
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3938
|
-
function
|
|
4277
|
+
function unknownKeys3(value, allowed2, path, errors) {
|
|
3939
4278
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3940
4279
|
}
|
|
3941
|
-
function
|
|
4280
|
+
function text3(value, path, errors, max = 65536) {
|
|
3942
4281
|
if (typeof value !== "string" || !value.trim()) {
|
|
3943
4282
|
errors.push(`${path} is required`);
|
|
3944
4283
|
return "";
|
|
@@ -3960,12 +4299,12 @@ function tiersOf(value, errors) {
|
|
|
3960
4299
|
errors.push(`${path} must be an object`);
|
|
3961
4300
|
return [];
|
|
3962
4301
|
}
|
|
3963
|
-
|
|
3964
|
-
const id4 =
|
|
4302
|
+
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
4303
|
+
const id4 = text3(item.id, `${path}.id`, errors, 80);
|
|
3965
4304
|
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3966
4305
|
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
3967
4306
|
seen.add(id4);
|
|
3968
|
-
const name =
|
|
4307
|
+
const name = text3(item.name, `${path}.name`, errors, 160);
|
|
3969
4308
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
3970
4309
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
3971
4310
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -3977,7 +4316,7 @@ function tiersOf(value, errors) {
|
|
|
3977
4316
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
3978
4317
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
3979
4318
|
}
|
|
3980
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
4319
|
+
const blurb = item.blurb === void 0 ? void 0 : text3(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
3981
4320
|
return [{
|
|
3982
4321
|
id: id4,
|
|
3983
4322
|
name,
|
|
@@ -3996,7 +4335,7 @@ function applicationOf(value, errors) {
|
|
|
3996
4335
|
errors.push("document.application must be an object");
|
|
3997
4336
|
return void 0;
|
|
3998
4337
|
}
|
|
3999
|
-
|
|
4338
|
+
unknownKeys3(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
4000
4339
|
try {
|
|
4001
4340
|
const resolved = resolveApplication(input);
|
|
4002
4341
|
const copy = recordOf(input.copy);
|
|
@@ -4022,11 +4361,11 @@ function policyOf(value, errors) {
|
|
|
4022
4361
|
return void 0;
|
|
4023
4362
|
}
|
|
4024
4363
|
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
4025
|
-
|
|
4364
|
+
unknownKeys3(input, allowed2, "document.policy", errors);
|
|
4026
4365
|
const result = {};
|
|
4027
4366
|
for (const key of allowed2) {
|
|
4028
4367
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
4029
|
-
result[key] =
|
|
4368
|
+
result[key] = text3(input[key], `document.policy.${key}`, errors);
|
|
4030
4369
|
}
|
|
4031
4370
|
return result;
|
|
4032
4371
|
}
|
|
@@ -4038,7 +4377,7 @@ function themeOf(value, errors) {
|
|
|
4038
4377
|
errors.push("document.theme.tokens must be an object");
|
|
4039
4378
|
return void 0;
|
|
4040
4379
|
}
|
|
4041
|
-
|
|
4380
|
+
unknownKeys3(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
4042
4381
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
4043
4382
|
let serializedLength = 0;
|
|
4044
4383
|
for (const [key, token] of Object.entries(tokens)) {
|
|
@@ -4058,17 +4397,17 @@ function validateSignupControlDocument(input) {
|
|
|
4058
4397
|
const errors = [];
|
|
4059
4398
|
const root = recordOf(input);
|
|
4060
4399
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
4061
|
-
|
|
4400
|
+
unknownKeys3(root, ROOT_KEYS4, "document", errors);
|
|
4062
4401
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
4063
4402
|
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
4064
4403
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
4065
4404
|
const source = recordOf(root.source);
|
|
4066
4405
|
const target = recordOf(root.target);
|
|
4067
|
-
const appId =
|
|
4068
|
-
const chapterId =
|
|
4069
|
-
const environment =
|
|
4070
|
-
if (appId && !
|
|
4071
|
-
if (chapterId && !
|
|
4406
|
+
const appId = text3(source?.appId, "document.source.appId", errors, 63);
|
|
4407
|
+
const chapterId = text3(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
4408
|
+
const environment = text3(target?.environment, "document.target.environment", errors, 63);
|
|
4409
|
+
if (appId && !SLUG7.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
4410
|
+
if (chapterId && !SLUG7.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
4072
4411
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
4073
4412
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
4074
4413
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
@@ -4218,7 +4557,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
|
4218
4557
|
|
|
4219
4558
|
// src/membership-markdown.ts
|
|
4220
4559
|
var MAX_BENEFITS2 = 24;
|
|
4221
|
-
var
|
|
4560
|
+
var MAX_MARKDOWN4 = 65536;
|
|
4222
4561
|
var MAX_LABEL2 = 160;
|
|
4223
4562
|
var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
|
|
4224
4563
|
var MARKDOWN_HEADING = /^#{1,6}\s+/;
|
|
@@ -4243,17 +4582,17 @@ function markdownBenefits(lines, path, errors) {
|
|
|
4243
4582
|
errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
|
|
4244
4583
|
continue;
|
|
4245
4584
|
}
|
|
4246
|
-
const
|
|
4247
|
-
if (
|
|
4248
|
-
if (
|
|
4585
|
+
const text4 = (match[1] ?? "").trim();
|
|
4586
|
+
if (text4.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
|
|
4587
|
+
if (text4) benefits.push(text4);
|
|
4249
4588
|
}
|
|
4250
4589
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
4251
4590
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
4252
4591
|
}
|
|
4253
4592
|
function applyMembershipTierMarkdown(presentation, source) {
|
|
4254
4593
|
if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
|
|
4255
|
-
if (source.length >
|
|
4256
|
-
return { ok: false, errors: [`membership Markdown must be at most ${
|
|
4594
|
+
if (source.length > MAX_MARKDOWN4) {
|
|
4595
|
+
return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN4.toLocaleString("en-US")} characters`] };
|
|
4257
4596
|
}
|
|
4258
4597
|
if (!source.trim()) return { ok: true, value: { ...presentation } };
|
|
4259
4598
|
const errors = [];
|