@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.js
CHANGED
|
@@ -471,6 +471,30 @@ var memberContentLocal = {
|
|
|
471
471
|
updatedBy: attr4("string", { optional: true })
|
|
472
472
|
}
|
|
473
473
|
};
|
|
474
|
+
var builderLibraryControls = {
|
|
475
|
+
attrs: {
|
|
476
|
+
id: id3(),
|
|
477
|
+
sourceId: attr4("string", { indexed: true }),
|
|
478
|
+
targetId: attr4("string", { indexed: true }),
|
|
479
|
+
environment: attr4("string", { indexed: true }),
|
|
480
|
+
revision: attr4("number", { indexed: true }),
|
|
481
|
+
digest: attr4("string", { indexed: true }),
|
|
482
|
+
document: attr4("json"),
|
|
483
|
+
active: attr4("boolean", { indexed: true }),
|
|
484
|
+
receivedAt: attr4("number", { indexed: true })
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
var builderLibraryLocal = {
|
|
488
|
+
attrs: {
|
|
489
|
+
id: id3(),
|
|
490
|
+
appId: attr4("string", { indexed: true }),
|
|
491
|
+
environment: attr4("string", { indexed: true }),
|
|
492
|
+
revision: attr4("number", { indexed: true }),
|
|
493
|
+
document: attr4("json"),
|
|
494
|
+
updatedAt: attr4("number", { indexed: true }),
|
|
495
|
+
updatedBy: attr4("string", { optional: true })
|
|
496
|
+
}
|
|
497
|
+
};
|
|
474
498
|
var groups = {
|
|
475
499
|
attrs: {
|
|
476
500
|
id: id3(),
|
|
@@ -542,7 +566,7 @@ var emailLog = {
|
|
|
542
566
|
sentAt: attr4("number", { indexed: true })
|
|
543
567
|
}
|
|
544
568
|
};
|
|
545
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
|
|
569
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false, includeBuilderLibrary = false) {
|
|
546
570
|
const entities = {};
|
|
547
571
|
if (mode === "chapter") {
|
|
548
572
|
entities.applications = applicationsFor(application);
|
|
@@ -566,6 +590,10 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
566
590
|
if (auth.source === "table") entities.admins = admins;
|
|
567
591
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
568
592
|
if (includeNetworkNotes) entities.networkNotes = networkNotes;
|
|
593
|
+
if (includeBuilderLibrary) {
|
|
594
|
+
entities.builderLibraryControls = builderLibraryControls;
|
|
595
|
+
entities.builderLibraryLocal = builderLibraryLocal;
|
|
596
|
+
}
|
|
569
597
|
const schema = { entities, links: {} };
|
|
570
598
|
const rules = {};
|
|
571
599
|
for (const ns of Object.keys(entities)) {
|
|
@@ -1201,8 +1229,8 @@ var APPLICATION_LABELS = {
|
|
|
1201
1229
|
state: "State"
|
|
1202
1230
|
};
|
|
1203
1231
|
function humanize(id4) {
|
|
1204
|
-
const
|
|
1205
|
-
return
|
|
1232
|
+
const text4 = id4.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1233
|
+
return text4 ? `${text4[0].toUpperCase()}${text4.slice(1)}` : id4;
|
|
1206
1234
|
}
|
|
1207
1235
|
function applicationDefs(chapter) {
|
|
1208
1236
|
try {
|
|
@@ -1493,15 +1521,47 @@ function resolveMemberContent(input) {
|
|
|
1493
1521
|
};
|
|
1494
1522
|
}
|
|
1495
1523
|
|
|
1496
|
-
// src/config.ts
|
|
1524
|
+
// src/config-builder-library.ts
|
|
1497
1525
|
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1526
|
+
var SECRET_NAME3 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1527
|
+
function resolveBuilderLibrary(mode, input) {
|
|
1528
|
+
if (!input) return null;
|
|
1529
|
+
if (input.sources !== void 0 && !Array.isArray(input.sources)) {
|
|
1530
|
+
throw new Error("defineChapter.builderLibrary.sources: must be an array");
|
|
1531
|
+
}
|
|
1532
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1533
|
+
const sources = (input.sources ?? []).map((source, index) => {
|
|
1534
|
+
if (!source || typeof source !== "object" || !SLUG4.test(source.sourceId)) {
|
|
1535
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index}].sourceId: must be a lowercase app slug`);
|
|
1536
|
+
}
|
|
1537
|
+
if (source.sourceId === "built-not-found" && mode === "hub") {
|
|
1538
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index}]: a hub accepts Chapter sources, not itself`);
|
|
1539
|
+
}
|
|
1540
|
+
if (mode === "chapter" && source.sourceId !== "built-not-found") {
|
|
1541
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index}]: Chapters may accept only the built-not-found source`);
|
|
1542
|
+
}
|
|
1543
|
+
if (seen.has(source.sourceId)) {
|
|
1544
|
+
throw new Error(`defineChapter.builderLibrary.sources: duplicate source "${source.sourceId}"`);
|
|
1545
|
+
}
|
|
1546
|
+
seen.add(source.sourceId);
|
|
1547
|
+
const secretName = source.secretName?.trim() || "builder_library_control_secret";
|
|
1548
|
+
if (!SECRET_NAME3.test(secretName)) {
|
|
1549
|
+
throw new Error(`defineChapter.builderLibrary.sources[${index}].secretName: must be a vault-key identifier`);
|
|
1550
|
+
}
|
|
1551
|
+
return { sourceId: source.sourceId, secretName };
|
|
1552
|
+
});
|
|
1553
|
+
return { enabled: true, localAuthoring: input.localAuthoring !== false, sources };
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/config.ts
|
|
1557
|
+
var SLUG5 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1498
1558
|
function isResolvedCrm(x) {
|
|
1499
1559
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1500
1560
|
}
|
|
1501
1561
|
function defineChapter(config) {
|
|
1502
1562
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1503
1563
|
const { id: id4, name } = config;
|
|
1504
|
-
if (typeof id4 !== "string" || !
|
|
1564
|
+
if (typeof id4 !== "string" || !SLUG5.test(id4)) {
|
|
1505
1565
|
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1506
1566
|
}
|
|
1507
1567
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
@@ -1527,6 +1587,7 @@ function defineChapter(config) {
|
|
|
1527
1587
|
const network = resolveNetwork(config, crm);
|
|
1528
1588
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1529
1589
|
const memberContent = resolveMemberContent(config.memberContent);
|
|
1590
|
+
const builderLibrary = resolveBuilderLibrary(mode, config.builderLibrary);
|
|
1530
1591
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1531
1592
|
if (signupControl && mode !== "chapter") {
|
|
1532
1593
|
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
@@ -1540,7 +1601,8 @@ function defineChapter(config) {
|
|
|
1540
1601
|
network.targets.length > 0,
|
|
1541
1602
|
application,
|
|
1542
1603
|
Boolean(signupControl),
|
|
1543
|
-
Boolean(memberContent)
|
|
1604
|
+
Boolean(memberContent),
|
|
1605
|
+
Boolean(builderLibrary)
|
|
1544
1606
|
);
|
|
1545
1607
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1546
1608
|
const account = config.account ?? "none";
|
|
@@ -1598,6 +1660,7 @@ function defineChapter(config) {
|
|
|
1598
1660
|
network,
|
|
1599
1661
|
signupControl,
|
|
1600
1662
|
memberContent,
|
|
1663
|
+
builderLibrary,
|
|
1601
1664
|
formation,
|
|
1602
1665
|
crm,
|
|
1603
1666
|
auth,
|
|
@@ -1848,6 +1911,13 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1848
1911
|
...emails?.debugEmail ? { debugEmail: emails.debugEmail } : {}
|
|
1849
1912
|
});
|
|
1850
1913
|
const seeds = [...crmDesc.seeds ?? []];
|
|
1914
|
+
const builderLibrarySecrets = [...new Set(
|
|
1915
|
+
chapter.builderLibrary?.sources.map((source) => source.secretName) ?? []
|
|
1916
|
+
)].map((name) => ({
|
|
1917
|
+
name,
|
|
1918
|
+
description: "Signing secret for private Builder Library delivery from a configured source.",
|
|
1919
|
+
required: true
|
|
1920
|
+
}));
|
|
1851
1921
|
const group = chapter.groupSeed();
|
|
1852
1922
|
if (group) {
|
|
1853
1923
|
seeds.push({ id: "group", ns: "groups", key: { attr: "id", value: chapter.id }, attrs: { ...group, createdAt: now } });
|
|
@@ -1880,6 +1950,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1880
1950
|
},
|
|
1881
1951
|
rules: { ...crmDesc.rules, ...chapter.rules },
|
|
1882
1952
|
seeds,
|
|
1953
|
+
secrets: builderLibrarySecrets,
|
|
1883
1954
|
probes: [
|
|
1884
1955
|
...crmDesc.probes ?? [],
|
|
1885
1956
|
// The snapshot route is deliberately private. A credential-free smoke
|
|
@@ -1896,6 +1967,11 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1896
1967
|
...chapter.memberContent ? [
|
|
1897
1968
|
{ path: "/api/member-content", expectedStatus: 401 },
|
|
1898
1969
|
{ path: "/api/admin/member-content", expectedStatus: 401 }
|
|
1970
|
+
] : [],
|
|
1971
|
+
...chapter.builderLibrary ? [
|
|
1972
|
+
{ path: "/api/builder-library-control", expectedStatus: 401 },
|
|
1973
|
+
{ path: "/api/admin/builder-library", expectedStatus: 401 },
|
|
1974
|
+
{ path: "/api/admin/builder-library/manifest", expectedStatus: 401 }
|
|
1899
1975
|
] : []
|
|
1900
1976
|
],
|
|
1901
1977
|
runbooks
|
|
@@ -1904,7 +1980,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1904
1980
|
|
|
1905
1981
|
// src/member-content-validation-helpers.ts
|
|
1906
1982
|
var ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
1907
|
-
var
|
|
1983
|
+
var SLUG6 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1908
1984
|
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
1909
1985
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
1910
1986
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
@@ -2014,7 +2090,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2014
2090
|
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2015
2091
|
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2016
2092
|
if (id4 && !ID.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2017
|
-
if (slug && !
|
|
2093
|
+
if (slug && !SLUG6.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2018
2094
|
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2019
2095
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2020
2096
|
ids.add(id4);
|
|
@@ -2200,9 +2276,9 @@ function canonicalValue(value) {
|
|
|
2200
2276
|
}
|
|
2201
2277
|
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2202
2278
|
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2203
|
-
const
|
|
2204
|
-
const entries = Object.keys(
|
|
2205
|
-
const item =
|
|
2279
|
+
const record4 = value;
|
|
2280
|
+
const entries = Object.keys(record4).sort().flatMap((key) => {
|
|
2281
|
+
const item = record4[key];
|
|
2206
2282
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2207
2283
|
});
|
|
2208
2284
|
return `{${entries.join(",")}}`;
|
|
@@ -2218,6 +2294,243 @@ async function createMemberContentEnvelope(document) {
|
|
|
2218
2294
|
return { document, digest: await memberContentDigest(document) };
|
|
2219
2295
|
}
|
|
2220
2296
|
|
|
2297
|
+
// src/builder-library-validation.ts
|
|
2298
|
+
var ID2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2299
|
+
var APP_ID = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
2300
|
+
var MAX_RESOURCES = 200;
|
|
2301
|
+
var MAX_MARKDOWN2 = 65536;
|
|
2302
|
+
var MAX_TEXT2 = 500;
|
|
2303
|
+
var MAX_TAGS = 20;
|
|
2304
|
+
var KINDS = /* @__PURE__ */ new Set(["document", "note", "link", "inspiration", "image", "file"]);
|
|
2305
|
+
var STATUSES = /* @__PURE__ */ new Set(["draft", "published", "archived"]);
|
|
2306
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "resources"]);
|
|
2307
|
+
var SOURCE_KEYS2 = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2308
|
+
var TARGET_KEYS2 = /* @__PURE__ */ new Set(["appId", "environment"]);
|
|
2309
|
+
var RESOURCE_KEYS = /* @__PURE__ */ new Set(["id", "kind", "title", "summary", "tags", "scope", "status", "revision", "bodyMarkdown", "url", "brandAsset", "provenance"]);
|
|
2310
|
+
var SCOPE_KEYS = /* @__PURE__ */ new Set(["kind", "chapterId"]);
|
|
2311
|
+
var ASSET_KEYS = /* @__PURE__ */ new Set(["bookId", "assetId", "filename"]);
|
|
2312
|
+
var PROVENANCE_KEYS = /* @__PURE__ */ new Set(["createdBy", "createdAt", "updatedBy", "updatedAt", "promotedFrom"]);
|
|
2313
|
+
var PROMOTION_KEYS = /* @__PURE__ */ new Set(["chapterId", "resourceId", "resourceRevision", "documentRevision", "promotedBy", "promotedAt"]);
|
|
2314
|
+
var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2315
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
2316
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2317
|
+
}
|
|
2318
|
+
function text(value, path, errors, max = MAX_TEXT2, required = true) {
|
|
2319
|
+
if (value === void 0 && !required) return void 0;
|
|
2320
|
+
if (typeof value !== "string" || required && !value.trim()) {
|
|
2321
|
+
errors.push(`${path} must be a non-empty string`);
|
|
2322
|
+
return void 0;
|
|
2323
|
+
}
|
|
2324
|
+
const result = value.trim();
|
|
2325
|
+
if (result.length > max) errors.push(`${path} must be at most ${max} characters`);
|
|
2326
|
+
return result;
|
|
2327
|
+
}
|
|
2328
|
+
function integer(value, path, errors, minimum = 1) {
|
|
2329
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum) errors.push(`${path} must be an integer >= ${minimum}`);
|
|
2330
|
+
return Number(value);
|
|
2331
|
+
}
|
|
2332
|
+
function scopeValue(value, path, errors) {
|
|
2333
|
+
const raw = record2(value);
|
|
2334
|
+
if (!raw) {
|
|
2335
|
+
errors.push(`${path} must be an object`);
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
unknownKeys2(raw, SCOPE_KEYS, path, errors);
|
|
2339
|
+
if (raw.kind === "global" || raw.kind === "built_not_found") {
|
|
2340
|
+
if (raw.chapterId !== void 0) errors.push(`${path}.chapterId is allowed only for chapter scope`);
|
|
2341
|
+
return { kind: raw.kind };
|
|
2342
|
+
}
|
|
2343
|
+
if (raw.kind === "chapter") {
|
|
2344
|
+
const chapterId = text(raw.chapterId, `${path}.chapterId`, errors, 63) ?? "";
|
|
2345
|
+
if (chapterId && !APP_ID.test(chapterId)) errors.push(`${path}.chapterId must be a lowercase app slug`);
|
|
2346
|
+
return { kind: "chapter", chapterId };
|
|
2347
|
+
}
|
|
2348
|
+
errors.push(`${path}.kind must be chapter, built_not_found, or global`);
|
|
2349
|
+
return null;
|
|
2350
|
+
}
|
|
2351
|
+
function resourceValue(value, path, errors) {
|
|
2352
|
+
const raw = record2(value);
|
|
2353
|
+
if (!raw) {
|
|
2354
|
+
errors.push(`${path} must be an object`);
|
|
2355
|
+
return null;
|
|
2356
|
+
}
|
|
2357
|
+
unknownKeys2(raw, RESOURCE_KEYS, path, errors);
|
|
2358
|
+
const id4 = text(raw.id, `${path}.id`, errors, 80) ?? "";
|
|
2359
|
+
if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be lowercase kebab-case`);
|
|
2360
|
+
const kind = text(raw.kind, `${path}.kind`, errors) ?? "";
|
|
2361
|
+
if (!KINDS.has(kind)) errors.push(`${path}.kind is unsupported`);
|
|
2362
|
+
const title = text(raw.title, `${path}.title`, errors, 240) ?? "";
|
|
2363
|
+
const summary = text(raw.summary, `${path}.summary`, errors, 1e3, false);
|
|
2364
|
+
const status = text(raw.status, `${path}.status`, errors) ?? "";
|
|
2365
|
+
if (!STATUSES.has(status)) errors.push(`${path}.status must be draft, published, or archived`);
|
|
2366
|
+
const revision = integer(raw.revision, `${path}.revision`, errors);
|
|
2367
|
+
const scope = scopeValue(raw.scope, `${path}.scope`, errors) ?? { kind: "global" };
|
|
2368
|
+
const tags = Array.isArray(raw.tags) ? raw.tags.map((tag, index) => text(tag, `${path}.tags[${index}]`, errors, 80) ?? "") : [];
|
|
2369
|
+
if (!Array.isArray(raw.tags)) errors.push(`${path}.tags must be an array`);
|
|
2370
|
+
if (tags.length > MAX_TAGS) errors.push(`${path}.tags may contain at most ${MAX_TAGS} items`);
|
|
2371
|
+
if (new Set(tags).size !== tags.length) errors.push(`${path}.tags must be unique`);
|
|
2372
|
+
const bodyMarkdown = text(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN2, false);
|
|
2373
|
+
const url = text(raw.url, `${path}.url`, errors, 2048, false);
|
|
2374
|
+
if (url) {
|
|
2375
|
+
try {
|
|
2376
|
+
if (new URL(url).protocol !== "https:") errors.push(`${path}.url must use https`);
|
|
2377
|
+
} catch {
|
|
2378
|
+
errors.push(`${path}.url must be a valid https URL`);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
const asset = raw.brandAsset === void 0 ? null : record2(raw.brandAsset);
|
|
2382
|
+
if (raw.brandAsset !== void 0 && !asset) errors.push(`${path}.brandAsset must be an object`);
|
|
2383
|
+
if (asset) unknownKeys2(asset, ASSET_KEYS, `${path}.brandAsset`, errors);
|
|
2384
|
+
const brandAsset = asset ? {
|
|
2385
|
+
bookId: text(asset.bookId, `${path}.brandAsset.bookId`, errors, 160) ?? "",
|
|
2386
|
+
assetId: text(asset.assetId, `${path}.brandAsset.assetId`, errors, 160) ?? "",
|
|
2387
|
+
...asset.filename === void 0 ? {} : { filename: text(asset.filename, `${path}.brandAsset.filename`, errors, 240) ?? "" }
|
|
2388
|
+
} : void 0;
|
|
2389
|
+
if ((kind === "document" || kind === "note") && !bodyMarkdown) errors.push(`${path}.bodyMarkdown is required for ${kind}`);
|
|
2390
|
+
if ((kind === "link" || kind === "inspiration") && !url) errors.push(`${path}.url is required for ${kind}`);
|
|
2391
|
+
if ((kind === "image" || kind === "file") && !brandAsset) errors.push(`${path}.brandAsset is required for ${kind}`);
|
|
2392
|
+
if (!["document", "note"].includes(kind) && bodyMarkdown) errors.push(`${path}.bodyMarkdown is not allowed for ${kind}`);
|
|
2393
|
+
if (!["link", "inspiration"].includes(kind) && url) errors.push(`${path}.url is not allowed for ${kind}`);
|
|
2394
|
+
if (!["image", "file"].includes(kind) && brandAsset) errors.push(`${path}.brandAsset is not allowed for ${kind}`);
|
|
2395
|
+
const provenanceRaw = record2(raw.provenance);
|
|
2396
|
+
if (!provenanceRaw) errors.push(`${path}.provenance must be an object`);
|
|
2397
|
+
if (provenanceRaw) unknownKeys2(provenanceRaw, PROVENANCE_KEYS, `${path}.provenance`, errors);
|
|
2398
|
+
const promotedRaw = provenanceRaw?.promotedFrom === void 0 ? null : record2(provenanceRaw.promotedFrom);
|
|
2399
|
+
if (provenanceRaw?.promotedFrom !== void 0 && !promotedRaw) errors.push(`${path}.provenance.promotedFrom must be an object`);
|
|
2400
|
+
if (promotedRaw) unknownKeys2(promotedRaw, PROMOTION_KEYS, `${path}.provenance.promotedFrom`, errors);
|
|
2401
|
+
const promotedFrom = promotedRaw ? {
|
|
2402
|
+
chapterId: text(promotedRaw.chapterId, `${path}.provenance.promotedFrom.chapterId`, errors, 63) ?? "",
|
|
2403
|
+
resourceId: text(promotedRaw.resourceId, `${path}.provenance.promotedFrom.resourceId`, errors, 80) ?? "",
|
|
2404
|
+
resourceRevision: integer(promotedRaw.resourceRevision, `${path}.provenance.promotedFrom.resourceRevision`, errors),
|
|
2405
|
+
documentRevision: integer(promotedRaw.documentRevision, `${path}.provenance.promotedFrom.documentRevision`, errors),
|
|
2406
|
+
promotedBy: text(promotedRaw.promotedBy, `${path}.provenance.promotedFrom.promotedBy`, errors, 320) ?? "",
|
|
2407
|
+
promotedAt: integer(promotedRaw.promotedAt, `${path}.provenance.promotedFrom.promotedAt`, errors, 0)
|
|
2408
|
+
} : void 0;
|
|
2409
|
+
const provenance = {
|
|
2410
|
+
createdBy: text(provenanceRaw?.createdBy, `${path}.provenance.createdBy`, errors, 320) ?? "",
|
|
2411
|
+
createdAt: integer(provenanceRaw?.createdAt, `${path}.provenance.createdAt`, errors, 0),
|
|
2412
|
+
updatedBy: text(provenanceRaw?.updatedBy, `${path}.provenance.updatedBy`, errors, 320) ?? "",
|
|
2413
|
+
updatedAt: integer(provenanceRaw?.updatedAt, `${path}.provenance.updatedAt`, errors, 0),
|
|
2414
|
+
...promotedFrom ? { promotedFrom } : {}
|
|
2415
|
+
};
|
|
2416
|
+
return { id: id4, kind, title, ...summary ? { summary } : {}, tags, scope, status, revision, ...bodyMarkdown ? { bodyMarkdown } : {}, ...url ? { url } : {}, ...brandAsset ? { brandAsset } : {}, provenance };
|
|
2417
|
+
}
|
|
2418
|
+
function validateBuilderLibraryDocument(value, options = {}) {
|
|
2419
|
+
const errors = [];
|
|
2420
|
+
const raw = record2(value);
|
|
2421
|
+
if (!raw) return { ok: false, errors: ["document must be an object"] };
|
|
2422
|
+
unknownKeys2(raw, ROOT_KEYS2, "document", errors);
|
|
2423
|
+
if (raw.schemaVersion !== BUILDER_LIBRARY_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2424
|
+
const revision = integer(raw.revision, "document.revision", errors);
|
|
2425
|
+
const generatedAt = integer(raw.generatedAt, "document.generatedAt", errors, 0);
|
|
2426
|
+
const sourceRaw = record2(raw.source);
|
|
2427
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2428
|
+
if (sourceRaw) unknownKeys2(sourceRaw, SOURCE_KEYS2, "document.source", errors);
|
|
2429
|
+
const sourceType = sourceRaw?.type;
|
|
2430
|
+
if (sourceType !== "chapter" && sourceType !== "built_not_found") errors.push("document.source.type must be chapter or built_not_found");
|
|
2431
|
+
const sourceAppId = text(sourceRaw?.appId, "document.source.appId", errors, 63) ?? "";
|
|
2432
|
+
if (sourceAppId && !APP_ID.test(sourceAppId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2433
|
+
if (options.sourceType && sourceType !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2434
|
+
if (options.sourceAppId && sourceAppId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2435
|
+
const targetRaw = raw.target === void 0 ? null : record2(raw.target);
|
|
2436
|
+
if (raw.target !== void 0 && !targetRaw) errors.push("document.target must be an object");
|
|
2437
|
+
if (targetRaw) unknownKeys2(targetRaw, TARGET_KEYS2, "document.target", errors);
|
|
2438
|
+
const target = targetRaw ? {
|
|
2439
|
+
appId: text(targetRaw.appId, "document.target.appId", errors, 63) ?? "",
|
|
2440
|
+
environment: text(targetRaw.environment, "document.target.environment", errors, 80) ?? ""
|
|
2441
|
+
} : void 0;
|
|
2442
|
+
if (options.targetAppId && target?.appId !== options.targetAppId) errors.push(`document.target.appId must be ${options.targetAppId}`);
|
|
2443
|
+
if (!Array.isArray(raw.resources)) errors.push("document.resources must be an array");
|
|
2444
|
+
const inputs = Array.isArray(raw.resources) ? raw.resources : [];
|
|
2445
|
+
if (inputs.length > MAX_RESOURCES) errors.push(`document.resources may contain at most ${MAX_RESOURCES} items`);
|
|
2446
|
+
const resources = inputs.map((item, index) => resourceValue(item, `document.resources[${index}]`, errors)).filter(Boolean);
|
|
2447
|
+
const ids = resources.map(({ id: id4 }) => id4);
|
|
2448
|
+
if (new Set(ids).size !== ids.length) errors.push("document.resources ids must be unique");
|
|
2449
|
+
for (const resource of resources) {
|
|
2450
|
+
if (sourceType === "chapter" && (resource.scope.kind !== "chapter" || resource.scope.chapterId !== sourceAppId)) {
|
|
2451
|
+
errors.push(`document.resources.${resource.id}: Chapter sources may author only their own chapter scope`);
|
|
2452
|
+
}
|
|
2453
|
+
if (sourceType === "built_not_found" && resource.scope.kind === "chapter") {
|
|
2454
|
+
errors.push(`document.resources.${resource.id}: Built Not Found sources may author only built_not_found or global scope`);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
if (errors.length) return { ok: false, errors };
|
|
2458
|
+
return { ok: true, value: { schemaVersion: 1, revision, generatedAt, source: { type: sourceType, appId: sourceAppId }, ...target ? { target } : {}, resources } };
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// src/builder-library.ts
|
|
2462
|
+
var BUILDER_LIBRARY_SCHEMA_VERSION = 1;
|
|
2463
|
+
var BUILDER_LIBRARY_ADMIN_PATH = "/api/admin/builder-library";
|
|
2464
|
+
var BUILDER_LIBRARY_MANIFEST_PATH = "/api/admin/builder-library/manifest";
|
|
2465
|
+
var BUILDER_LIBRARY_PROMOTE_PATH = "/api/admin/builder-library/promote";
|
|
2466
|
+
var BUILDER_LIBRARY_CONTROL_PATH = "/api/builder-library-control";
|
|
2467
|
+
function canViewBuilderLibraryResource(resource, viewer) {
|
|
2468
|
+
if (viewer.kind === "built_not_found") return true;
|
|
2469
|
+
return resource.scope.kind === "global" || resource.scope.kind === "chapter" && resource.scope.chapterId === viewer.chapterId;
|
|
2470
|
+
}
|
|
2471
|
+
function buildBuilderLibraryManifest(documents, viewer, generatedAt = Date.now(), limit = 100) {
|
|
2472
|
+
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));
|
|
2473
|
+
return {
|
|
2474
|
+
schemaVersion: BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
2475
|
+
generatedAt,
|
|
2476
|
+
viewer,
|
|
2477
|
+
revisions: documents.map(({ source, revision }) => ({ source, revision })),
|
|
2478
|
+
resources: ordered.slice(0, limit).map(({ resource }) => resource),
|
|
2479
|
+
truncated: ordered.length > limit
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
function projectBuilderLibraryDelivery(document, target) {
|
|
2483
|
+
return {
|
|
2484
|
+
...document,
|
|
2485
|
+
target,
|
|
2486
|
+
resources: document.source.type === "built_not_found" ? document.resources.filter((resource) => resource.scope.kind === "global") : document.resources
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
function promoteBuilderLibraryResource(input) {
|
|
2490
|
+
if (input.resource.scope.kind !== "chapter") throw new TypeError("only a Chapter-scoped resource can be promoted");
|
|
2491
|
+
const chapterId = input.resource.scope.chapterId;
|
|
2492
|
+
return {
|
|
2493
|
+
...input.resource,
|
|
2494
|
+
id: input.globalId ?? `${chapterId}-${input.resource.id}`,
|
|
2495
|
+
scope: { kind: "global" },
|
|
2496
|
+
revision: 1,
|
|
2497
|
+
provenance: {
|
|
2498
|
+
createdBy: input.actor,
|
|
2499
|
+
createdAt: input.promotedAt,
|
|
2500
|
+
updatedBy: input.actor,
|
|
2501
|
+
updatedAt: input.promotedAt,
|
|
2502
|
+
promotedFrom: {
|
|
2503
|
+
chapterId,
|
|
2504
|
+
resourceId: input.resource.id,
|
|
2505
|
+
resourceRevision: input.resource.revision,
|
|
2506
|
+
documentRevision: input.chapterDocumentRevision,
|
|
2507
|
+
promotedBy: input.actor,
|
|
2508
|
+
promotedAt: input.promotedAt
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// src/builder-library-canonical.ts
|
|
2515
|
+
var encoder2 = new TextEncoder();
|
|
2516
|
+
function canonicalValue2(value) {
|
|
2517
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") return JSON.stringify(value);
|
|
2518
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2519
|
+
if (typeof value !== "object") throw new TypeError("Builder Library contains an unsupported value");
|
|
2520
|
+
const record4 = value;
|
|
2521
|
+
return `{${Object.keys(record4).sort().flatMap((key) => record4[key] === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(record4[key])}`]).join(",")}}`;
|
|
2522
|
+
}
|
|
2523
|
+
function canonicalBuilderLibrary(value) {
|
|
2524
|
+
return canonicalValue2(value);
|
|
2525
|
+
}
|
|
2526
|
+
async function builderLibraryDigest(document) {
|
|
2527
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalBuilderLibrary(document)));
|
|
2528
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2529
|
+
}
|
|
2530
|
+
async function createBuilderLibraryEnvelope(document) {
|
|
2531
|
+
return { document, digest: await builderLibraryDigest(document) };
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2221
2534
|
// src/email.ts
|
|
2222
2535
|
function render(template, vars) {
|
|
2223
2536
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -2248,10 +2561,10 @@ function planDelivery(input) {
|
|
|
2248
2561
|
const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
|
|
2249
2562
|
const to = redirect ? input.group.debugEmail : input.to;
|
|
2250
2563
|
const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
|
|
2251
|
-
const
|
|
2564
|
+
const text4 = redirect ? `(dev redirect; original recipient: ${input.to})
|
|
2252
2565
|
|
|
2253
2566
|
` + render(tpl.text, vars) : render(tpl.text, vars);
|
|
2254
|
-
return { deliver: true, transport, to, subject, text:
|
|
2567
|
+
return { deliver: true, transport, to, subject, text: text4, redirected: redirect };
|
|
2255
2568
|
}
|
|
2256
2569
|
|
|
2257
2570
|
// src/notify.ts
|
|
@@ -2334,9 +2647,21 @@ function webhookMutationId(eventId) {
|
|
|
2334
2647
|
function findApplicationRef(obj) {
|
|
2335
2648
|
const metaOf = (v) => v && typeof v === "object" ? v.metadata ?? {} : {};
|
|
2336
2649
|
const pick = (m) => typeof m.applicationId === "string" ? m.applicationId : void 0;
|
|
2337
|
-
const
|
|
2650
|
+
const candidates = [
|
|
2651
|
+
metaOf(obj),
|
|
2652
|
+
metaOf(obj.subscription_details),
|
|
2653
|
+
metaOf(obj.parent?.subscription_details)
|
|
2654
|
+
];
|
|
2655
|
+
const applicationId = candidates.map(pick).find(Boolean);
|
|
2656
|
+
const appId = candidates.map((metadata) => typeof metadata.odlaAppId === "string" ? metadata.odlaAppId : void 0).find(Boolean);
|
|
2657
|
+
const environment = candidates.map((metadata) => typeof metadata.odlaEnvironment === "string" ? metadata.odlaEnvironment : void 0).find(Boolean);
|
|
2338
2658
|
const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
|
|
2339
|
-
return {
|
|
2659
|
+
return {
|
|
2660
|
+
...applicationId ? { applicationId } : {},
|
|
2661
|
+
...customerId ? { customerId } : {},
|
|
2662
|
+
...appId ? { appId } : {},
|
|
2663
|
+
...environment ? { environment } : {}
|
|
2664
|
+
};
|
|
2340
2665
|
}
|
|
2341
2666
|
function findGiftRef(obj) {
|
|
2342
2667
|
const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
|
|
@@ -2396,25 +2721,25 @@ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
|
2396
2721
|
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
2397
2722
|
|
|
2398
2723
|
// src/signup-control-canonical.ts
|
|
2399
|
-
var
|
|
2400
|
-
function
|
|
2724
|
+
var encoder3 = new TextEncoder();
|
|
2725
|
+
function canonicalValue3(value) {
|
|
2401
2726
|
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2402
2727
|
return JSON.stringify(value);
|
|
2403
2728
|
}
|
|
2404
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2729
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue3).join(",")}]`;
|
|
2405
2730
|
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2406
|
-
const
|
|
2407
|
-
const entries = Object.keys(
|
|
2408
|
-
const item =
|
|
2409
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${
|
|
2731
|
+
const record4 = value;
|
|
2732
|
+
const entries = Object.keys(record4).sort().flatMap((key) => {
|
|
2733
|
+
const item = record4[key];
|
|
2734
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue3(item)}`];
|
|
2410
2735
|
});
|
|
2411
2736
|
return `{${entries.join(",")}}`;
|
|
2412
2737
|
}
|
|
2413
2738
|
function canonicalSignupControl(value) {
|
|
2414
|
-
return
|
|
2739
|
+
return canonicalValue3(value);
|
|
2415
2740
|
}
|
|
2416
2741
|
async function signupControlDigest(document) {
|
|
2417
|
-
const digest = await crypto.subtle.digest("SHA-256",
|
|
2742
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder3.encode(canonicalSignupControl(document)));
|
|
2418
2743
|
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2419
2744
|
}
|
|
2420
2745
|
async function createSignupControlEnvelope(document) {
|
|
@@ -2422,7 +2747,7 @@ async function createSignupControlEnvelope(document) {
|
|
|
2422
2747
|
}
|
|
2423
2748
|
|
|
2424
2749
|
// src/admission-grants.ts
|
|
2425
|
-
var
|
|
2750
|
+
var ID3 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
2426
2751
|
var MAX_GRANTS = 2e3;
|
|
2427
2752
|
var MAX_NOTE = 2e3;
|
|
2428
2753
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -2444,10 +2769,10 @@ function validateAdmissionGrantDocument(input) {
|
|
|
2444
2769
|
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
2445
2770
|
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
2446
2771
|
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
2447
|
-
if (!
|
|
2772
|
+
if (!ID3.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
2448
2773
|
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
2449
|
-
if (!
|
|
2450
|
-
if (!
|
|
2774
|
+
if (!ID3.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
2775
|
+
if (!ID3.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
2451
2776
|
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
2452
2777
|
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
2453
2778
|
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
@@ -2462,7 +2787,7 @@ function validateAdmissionGrantDocument(input) {
|
|
|
2462
2787
|
const decision = grant.decision;
|
|
2463
2788
|
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
2464
2789
|
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
2465
|
-
if (!
|
|
2790
|
+
if (!ID3.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
2466
2791
|
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
2467
2792
|
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
2468
2793
|
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
@@ -2619,11 +2944,11 @@ function resolveJoinContinuation(input) {
|
|
|
2619
2944
|
// src/gift-offers.ts
|
|
2620
2945
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2621
2946
|
var MAX_GIFT_OFFERS = 20;
|
|
2622
|
-
var
|
|
2947
|
+
var ID4 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2623
2948
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2624
2949
|
var CURRENCY = /^[a-z]{3}$/;
|
|
2625
|
-
var
|
|
2626
|
-
var
|
|
2950
|
+
var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2951
|
+
var text2 = (value, path, errors, max) => {
|
|
2627
2952
|
if (typeof value !== "string" || !value.trim()) {
|
|
2628
2953
|
errors.push(`${path} is required`);
|
|
2629
2954
|
return "";
|
|
@@ -2633,7 +2958,7 @@ var text = (value, path, errors, max) => {
|
|
|
2633
2958
|
};
|
|
2634
2959
|
function validateGiftOfferCatalog(input, tierIds) {
|
|
2635
2960
|
const errors = [];
|
|
2636
|
-
const root =
|
|
2961
|
+
const root = record3(input);
|
|
2637
2962
|
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2638
2963
|
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2639
2964
|
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
@@ -2644,30 +2969,30 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2644
2969
|
const offers = [];
|
|
2645
2970
|
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2646
2971
|
const path = `document.giftOffers.offers[${index}]`;
|
|
2647
|
-
const item =
|
|
2972
|
+
const item = record3(raw);
|
|
2648
2973
|
if (!item) {
|
|
2649
2974
|
errors.push(`${path} must be an object`);
|
|
2650
2975
|
continue;
|
|
2651
2976
|
}
|
|
2652
2977
|
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2653
2978
|
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2654
|
-
const id4 =
|
|
2655
|
-
if (id4 && !
|
|
2979
|
+
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
2980
|
+
if (id4 && !ID4.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2656
2981
|
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2657
2982
|
ids.add(id4);
|
|
2658
|
-
const targetRaw =
|
|
2983
|
+
const targetRaw = record3(item.target);
|
|
2659
2984
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2660
2985
|
if (targetRaw?.type === "tier") {
|
|
2661
|
-
const tierId =
|
|
2986
|
+
const tierId = text2(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
2662
2987
|
if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
|
|
2663
2988
|
target = { type: "tier", tierId };
|
|
2664
2989
|
} else if (targetRaw?.type === "entitlement") {
|
|
2665
|
-
const entitlementKey =
|
|
2666
|
-
if (entitlementKey && !
|
|
2990
|
+
const entitlementKey = text2(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
2991
|
+
if (entitlementKey && !ID4.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2667
2992
|
target = { type: "entitlement", entitlementKey };
|
|
2668
2993
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2669
2994
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
2670
|
-
const stripePriceId =
|
|
2995
|
+
const stripePriceId = text2(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
|
|
2671
2996
|
if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
|
|
2672
2997
|
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`);
|
|
2673
2998
|
const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
|
|
@@ -2689,9 +3014,9 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2689
3014
|
redemptionLifetimeHours: lifetime,
|
|
2690
3015
|
...activeFrom !== void 0 ? { activeFrom } : {},
|
|
2691
3016
|
...activeUntil !== void 0 ? { activeUntil } : {},
|
|
2692
|
-
title:
|
|
2693
|
-
description:
|
|
2694
|
-
ctaLabel:
|
|
3017
|
+
title: text2(item.title, `${path}.title`, errors, 160),
|
|
3018
|
+
description: text2(item.description, `${path}.description`, errors, 2e3),
|
|
3019
|
+
ctaLabel: text2(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
|
|
2695
3020
|
});
|
|
2696
3021
|
}
|
|
2697
3022
|
return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
|
|
@@ -2748,34 +3073,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
2748
3073
|
if (readable && raw.length <= 64) return raw;
|
|
2749
3074
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
2750
3075
|
}
|
|
2751
|
-
function normalizeSharedRecord(
|
|
2752
|
-
if ("input" in
|
|
3076
|
+
function normalizeSharedRecord(record4, fallbackSourceId = "legacy-source") {
|
|
3077
|
+
if ("input" in record4 && record4.version === 2) {
|
|
2753
3078
|
return {
|
|
2754
3079
|
version: 2,
|
|
2755
|
-
sourceId:
|
|
2756
|
-
sourceRecordId:
|
|
2757
|
-
type:
|
|
2758
|
-
input:
|
|
3080
|
+
sourceId: record4.source.siteId,
|
|
3081
|
+
sourceRecordId: record4.source.recordId,
|
|
3082
|
+
type: record4.type,
|
|
3083
|
+
input: record4.input
|
|
2759
3084
|
};
|
|
2760
3085
|
}
|
|
2761
|
-
if ("input" in
|
|
3086
|
+
if ("input" in record4) {
|
|
2762
3087
|
return {
|
|
2763
3088
|
version: 1,
|
|
2764
3089
|
sourceId: fallbackSourceId,
|
|
2765
|
-
sourceRecordId:
|
|
2766
|
-
type:
|
|
2767
|
-
input:
|
|
3090
|
+
sourceRecordId: record4.hubRecordId,
|
|
3091
|
+
type: record4.type,
|
|
3092
|
+
input: record4.input
|
|
2768
3093
|
};
|
|
2769
3094
|
}
|
|
2770
|
-
if ("type" in
|
|
2771
|
-
const input = { name:
|
|
3095
|
+
if ("type" in record4 && record4.type === "company") {
|
|
3096
|
+
const input = { name: record4.name };
|
|
2772
3097
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
2773
|
-
if (
|
|
3098
|
+
if (record4[key]) input[key] = record4[key];
|
|
2774
3099
|
}
|
|
2775
3100
|
return {
|
|
2776
3101
|
version: 1,
|
|
2777
3102
|
sourceId: fallbackSourceId,
|
|
2778
|
-
sourceRecordId:
|
|
3103
|
+
sourceRecordId: record4.hubRecordId,
|
|
2779
3104
|
type: "company",
|
|
2780
3105
|
input
|
|
2781
3106
|
};
|
|
@@ -2783,28 +3108,28 @@ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
|
2783
3108
|
return {
|
|
2784
3109
|
version: 1,
|
|
2785
3110
|
sourceId: fallbackSourceId,
|
|
2786
|
-
sourceRecordId:
|
|
3111
|
+
sourceRecordId: record4.hubRecordId,
|
|
2787
3112
|
type: "person",
|
|
2788
|
-
input: sharedPersonInput(
|
|
3113
|
+
input: sharedPersonInput(record4)
|
|
2789
3114
|
};
|
|
2790
3115
|
}
|
|
2791
|
-
function sharedRecordFromCrm(crm,
|
|
2792
|
-
if (target.fields && !target.fields[
|
|
2793
|
-
throw new Error(`${target.name} does not accept "${
|
|
3116
|
+
function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
3117
|
+
if (target.fields && !target.fields[record4.type]) {
|
|
3118
|
+
throw new Error(`${target.name} does not accept "${record4.type}" records`);
|
|
2794
3119
|
}
|
|
2795
|
-
const def = crm.type(
|
|
2796
|
-
const fields = target.fields?.[
|
|
3120
|
+
const def = crm.type(record4.type);
|
|
3121
|
+
const fields = target.fields?.[record4.type] ?? DEFAULT_SHARE_FIELDS[record4.type];
|
|
2797
3122
|
if (!fields) {
|
|
2798
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
3123
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record4.type}" records`);
|
|
2799
3124
|
}
|
|
2800
3125
|
const nameField = def.nameField ?? "name";
|
|
2801
3126
|
const input = {};
|
|
2802
3127
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
2803
|
-
const value =
|
|
3128
|
+
const value = record4.fields?.[field];
|
|
2804
3129
|
if (value !== void 0) input[field] = value;
|
|
2805
3130
|
}
|
|
2806
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
2807
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
3131
|
+
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
3132
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
2808
3133
|
}
|
|
2809
3134
|
async function upsertPerson(deps, opts) {
|
|
2810
3135
|
const email = opts.email.toLowerCase();
|
|
@@ -2818,11 +3143,11 @@ async function upsertPerson(deps, opts) {
|
|
|
2818
3143
|
const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
2819
3144
|
return { recordId: created.id };
|
|
2820
3145
|
}
|
|
2821
|
-
async function findSharedRecord(deps,
|
|
3146
|
+
async function findSharedRecord(deps, record4, tag) {
|
|
2822
3147
|
const structured = await getRecordByOrigin(
|
|
2823
3148
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
2824
|
-
|
|
2825
|
-
|
|
3149
|
+
record4.sourceId,
|
|
3150
|
+
record4.sourceRecordId
|
|
2826
3151
|
);
|
|
2827
3152
|
if (structured) return structured;
|
|
2828
3153
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -2831,53 +3156,53 @@ async function findSharedRecord(deps, record3, tag) {
|
|
|
2831
3156
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
2832
3157
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2833
3158
|
}
|
|
2834
|
-
const def = deps.crm.type(
|
|
3159
|
+
const def = deps.crm.type(record4.type);
|
|
2835
3160
|
const emailField = def.emailField;
|
|
2836
|
-
if (emailField && typeof
|
|
2837
|
-
const primaryEmail =
|
|
3161
|
+
if (emailField && typeof record4.input[emailField] === "string") {
|
|
3162
|
+
const primaryEmail = record4.input[emailField].toLowerCase();
|
|
2838
3163
|
const found = await deps.db.query({
|
|
2839
|
-
crm_record: { $: { where: { type:
|
|
3164
|
+
crm_record: { $: { where: { type: record4.type, primaryEmail }, limit: 1 } }
|
|
2840
3165
|
});
|
|
2841
3166
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2842
3167
|
}
|
|
2843
|
-
const domain =
|
|
3168
|
+
const domain = record4.input.domain;
|
|
2844
3169
|
const domainSlot = def.fields.domain?.slot;
|
|
2845
3170
|
if (typeof domain === "string" && domainSlot) {
|
|
2846
3171
|
const found = await deps.db.query({
|
|
2847
|
-
crm_record: { $: { where: { type:
|
|
3172
|
+
crm_record: { $: { where: { type: record4.type, [domainSlot]: domain }, limit: 1 } }
|
|
2848
3173
|
});
|
|
2849
3174
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2850
3175
|
}
|
|
2851
3176
|
const nameField = def.nameField ?? "name";
|
|
2852
|
-
const name =
|
|
2853
|
-
if (
|
|
3177
|
+
const name = record4.input[nameField];
|
|
3178
|
+
if (record4.type === "company" && typeof name === "string" && name.trim()) {
|
|
2854
3179
|
const found = await deps.db.query({
|
|
2855
|
-
crm_record: { $: { where: { type:
|
|
3180
|
+
crm_record: { $: { where: { type: record4.type, name: name.trim() }, limit: 1 } }
|
|
2856
3181
|
});
|
|
2857
3182
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2858
3183
|
}
|
|
2859
3184
|
return void 0;
|
|
2860
3185
|
}
|
|
2861
3186
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
2862
|
-
const
|
|
2863
|
-
if (!
|
|
3187
|
+
const record4 = normalizeSharedRecord(shared, options.sourceId);
|
|
3188
|
+
if (!record4.type.trim() || !record4.sourceId.trim() || !record4.sourceRecordId.trim()) {
|
|
2864
3189
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
2865
3190
|
}
|
|
2866
|
-
if (options.sourceId && options.sourceId !==
|
|
3191
|
+
if (options.sourceId && options.sourceId !== record4.sourceId) {
|
|
2867
3192
|
throw new Error("signed sender does not match payload source");
|
|
2868
3193
|
}
|
|
2869
|
-
const tag = networkSourceTag(
|
|
3194
|
+
const tag = networkSourceTag(record4.type, record4.sourceRecordId, record4.version === 2 ? record4.sourceId : void 0);
|
|
2870
3195
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
2871
|
-
const existing = await findSharedRecord(deps,
|
|
3196
|
+
const existing = await findSharedRecord(deps, record4, tag);
|
|
2872
3197
|
let recordId;
|
|
2873
3198
|
if (existing && typeof existing.id === "string") {
|
|
2874
|
-
await updateRecord(crmDeps, { id: existing.id, input:
|
|
3199
|
+
await updateRecord(crmDeps, { id: existing.id, input: record4.input });
|
|
2875
3200
|
recordId = existing.id;
|
|
2876
3201
|
} else {
|
|
2877
|
-
recordId = `network_${shortHash(`${
|
|
3202
|
+
recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
|
|
2878
3203
|
await createRecord({ ...crmDeps, newId: () => recordId }, {
|
|
2879
|
-
type:
|
|
2880
|
-
input:
|
|
3204
|
+
type: record4.type,
|
|
3205
|
+
input: record4.input,
|
|
2881
3206
|
mutationId: `share-create:${tag}`
|
|
2882
3207
|
});
|
|
2883
3208
|
}
|
|
@@ -2887,10 +3212,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
2887
3212
|
);
|
|
2888
3213
|
await upsertRecordOrigin(crmDeps, {
|
|
2889
3214
|
recordId,
|
|
2890
|
-
sourceId:
|
|
2891
|
-
sourceRecordId:
|
|
3215
|
+
sourceId: record4.sourceId,
|
|
3216
|
+
sourceRecordId: record4.sourceRecordId,
|
|
2892
3217
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
2893
|
-
payloadVersion:
|
|
3218
|
+
payloadVersion: record4.version
|
|
2894
3219
|
});
|
|
2895
3220
|
return { recordId };
|
|
2896
3221
|
}
|
|
@@ -3174,8 +3499,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
3174
3499
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
3175
3500
|
const page = Array.isArray(data) ? data : [];
|
|
3176
3501
|
for (const u of page) {
|
|
3177
|
-
const
|
|
3178
|
-
if (
|
|
3502
|
+
const record4 = toRecord(u);
|
|
3503
|
+
if (record4) out.push(record4);
|
|
3179
3504
|
}
|
|
3180
3505
|
if (page.length < PAGE) break;
|
|
3181
3506
|
}
|
|
@@ -3582,7 +3907,7 @@ var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
|
3582
3907
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
3583
3908
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3584
3909
|
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
3585
|
-
var
|
|
3910
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
3586
3911
|
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
3587
3912
|
"tierId",
|
|
3588
3913
|
"variant",
|
|
@@ -3601,7 +3926,7 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
|
3601
3926
|
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
3602
3927
|
var MAX_TIERS = 20;
|
|
3603
3928
|
var MAX_BENEFITS = 24;
|
|
3604
|
-
var
|
|
3929
|
+
var MAX_MARKDOWN3 = 65536;
|
|
3605
3930
|
var MAX_LABEL = 160;
|
|
3606
3931
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3607
3932
|
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
@@ -3618,16 +3943,16 @@ function boundedString(value, path, errors, max, required = false) {
|
|
|
3618
3943
|
errors.push(`${path} must be a string`);
|
|
3619
3944
|
return void 0;
|
|
3620
3945
|
}
|
|
3621
|
-
const
|
|
3622
|
-
if (required && !
|
|
3623
|
-
if (
|
|
3624
|
-
return
|
|
3946
|
+
const text4 = value.trim();
|
|
3947
|
+
if (required && !text4) errors.push(`${path} is required`);
|
|
3948
|
+
if (text4.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
3949
|
+
return text4;
|
|
3625
3950
|
}
|
|
3626
3951
|
function validateMembershipPageDocument(input, options = {}) {
|
|
3627
3952
|
const errors = [];
|
|
3628
3953
|
const root = objectRecord(input);
|
|
3629
3954
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3630
|
-
rejectUnknownKeys(root,
|
|
3955
|
+
rejectUnknownKeys(root, ROOT_KEYS3, "document", errors);
|
|
3631
3956
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3632
3957
|
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
3633
3958
|
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
@@ -3670,8 +3995,8 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3670
3995
|
else {
|
|
3671
3996
|
if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
|
|
3672
3997
|
benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
|
|
3673
|
-
const
|
|
3674
|
-
return
|
|
3998
|
+
const text4 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
|
|
3999
|
+
return text4 ? [text4] : [];
|
|
3675
4000
|
});
|
|
3676
4001
|
}
|
|
3677
4002
|
}
|
|
@@ -3681,7 +4006,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3681
4006
|
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
3682
4007
|
else {
|
|
3683
4008
|
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
3684
|
-
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors,
|
|
4009
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN3, true);
|
|
3685
4010
|
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
3686
4011
|
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
3687
4012
|
}
|
|
@@ -3703,10 +4028,10 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3703
4028
|
}
|
|
3704
4029
|
}
|
|
3705
4030
|
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
3706
|
-
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors,
|
|
4031
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN3);
|
|
3707
4032
|
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
3708
4033
|
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
3709
|
-
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors,
|
|
4034
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN3);
|
|
3710
4035
|
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
3711
4036
|
tiers2.push({
|
|
3712
4037
|
tierId,
|
|
@@ -3728,7 +4053,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3728
4053
|
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
3729
4054
|
}
|
|
3730
4055
|
}
|
|
3731
|
-
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors,
|
|
4056
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN3);
|
|
3732
4057
|
if (errors.length) return { ok: false, errors };
|
|
3733
4058
|
return {
|
|
3734
4059
|
ok: true,
|
|
@@ -3747,11 +4072,11 @@ function membershipPageDocument(input, options = {}) {
|
|
|
3747
4072
|
}
|
|
3748
4073
|
|
|
3749
4074
|
// src/signup-control-validate.ts
|
|
3750
|
-
var
|
|
4075
|
+
var SLUG7 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3751
4076
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
3752
4077
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3753
4078
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
3754
|
-
var
|
|
4079
|
+
var ROOT_KEYS4 = /* @__PURE__ */ new Set([
|
|
3755
4080
|
"schemaVersion",
|
|
3756
4081
|
"revision",
|
|
3757
4082
|
"generatedAt",
|
|
@@ -3767,10 +4092,10 @@ var ROOT_KEYS3 = /* @__PURE__ */ new Set([
|
|
|
3767
4092
|
]);
|
|
3768
4093
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
3769
4094
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3770
|
-
function
|
|
4095
|
+
function unknownKeys3(value, allowed2, path, errors) {
|
|
3771
4096
|
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3772
4097
|
}
|
|
3773
|
-
function
|
|
4098
|
+
function text3(value, path, errors, max = 65536) {
|
|
3774
4099
|
if (typeof value !== "string" || !value.trim()) {
|
|
3775
4100
|
errors.push(`${path} is required`);
|
|
3776
4101
|
return "";
|
|
@@ -3792,12 +4117,12 @@ function tiersOf(value, errors) {
|
|
|
3792
4117
|
errors.push(`${path} must be an object`);
|
|
3793
4118
|
return [];
|
|
3794
4119
|
}
|
|
3795
|
-
|
|
3796
|
-
const id4 =
|
|
4120
|
+
unknownKeys3(item, TIER_KEYS2, path, errors);
|
|
4121
|
+
const id4 = text3(item.id, `${path}.id`, errors, 80);
|
|
3797
4122
|
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3798
4123
|
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
3799
4124
|
seen.add(id4);
|
|
3800
|
-
const name =
|
|
4125
|
+
const name = text3(item.name, `${path}.name`, errors, 160);
|
|
3801
4126
|
if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
|
|
3802
4127
|
if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
|
|
3803
4128
|
if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
|
|
@@ -3809,7 +4134,7 @@ function tiersOf(value, errors) {
|
|
|
3809
4134
|
if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
|
|
3810
4135
|
errors.push(`${path}.stripePriceId is required for an available paid tier`);
|
|
3811
4136
|
}
|
|
3812
|
-
const blurb = item.blurb === void 0 ? void 0 :
|
|
4137
|
+
const blurb = item.blurb === void 0 ? void 0 : text3(item.blurb, `${path}.blurb`, errors, 4e3);
|
|
3813
4138
|
return [{
|
|
3814
4139
|
id: id4,
|
|
3815
4140
|
name,
|
|
@@ -3828,7 +4153,7 @@ function applicationOf(value, errors) {
|
|
|
3828
4153
|
errors.push("document.application must be an object");
|
|
3829
4154
|
return void 0;
|
|
3830
4155
|
}
|
|
3831
|
-
|
|
4156
|
+
unknownKeys3(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
3832
4157
|
try {
|
|
3833
4158
|
const resolved = resolveApplication(input);
|
|
3834
4159
|
const copy = recordOf(input.copy);
|
|
@@ -3854,11 +4179,11 @@ function policyOf(value, errors) {
|
|
|
3854
4179
|
return void 0;
|
|
3855
4180
|
}
|
|
3856
4181
|
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
3857
|
-
|
|
4182
|
+
unknownKeys3(input, allowed2, "document.policy", errors);
|
|
3858
4183
|
const result = {};
|
|
3859
4184
|
for (const key of allowed2) {
|
|
3860
4185
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3861
|
-
result[key] =
|
|
4186
|
+
result[key] = text3(input[key], `document.policy.${key}`, errors);
|
|
3862
4187
|
}
|
|
3863
4188
|
return result;
|
|
3864
4189
|
}
|
|
@@ -3870,7 +4195,7 @@ function themeOf(value, errors) {
|
|
|
3870
4195
|
errors.push("document.theme.tokens must be an object");
|
|
3871
4196
|
return void 0;
|
|
3872
4197
|
}
|
|
3873
|
-
|
|
4198
|
+
unknownKeys3(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
3874
4199
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
3875
4200
|
let serializedLength = 0;
|
|
3876
4201
|
for (const [key, token] of Object.entries(tokens)) {
|
|
@@ -3890,17 +4215,17 @@ function validateSignupControlDocument(input) {
|
|
|
3890
4215
|
const errors = [];
|
|
3891
4216
|
const root = recordOf(input);
|
|
3892
4217
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3893
|
-
|
|
4218
|
+
unknownKeys3(root, ROOT_KEYS4, "document", errors);
|
|
3894
4219
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3895
4220
|
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
3896
4221
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
3897
4222
|
const source = recordOf(root.source);
|
|
3898
4223
|
const target = recordOf(root.target);
|
|
3899
|
-
const appId =
|
|
3900
|
-
const chapterId =
|
|
3901
|
-
const environment =
|
|
3902
|
-
if (appId && !
|
|
3903
|
-
if (chapterId && !
|
|
4224
|
+
const appId = text3(source?.appId, "document.source.appId", errors, 63);
|
|
4225
|
+
const chapterId = text3(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
4226
|
+
const environment = text3(target?.environment, "document.target.environment", errors, 63);
|
|
4227
|
+
if (appId && !SLUG7.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
4228
|
+
if (chapterId && !SLUG7.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3904
4229
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
3905
4230
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
3906
4231
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
@@ -4050,7 +4375,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
|
4050
4375
|
|
|
4051
4376
|
// src/membership-markdown.ts
|
|
4052
4377
|
var MAX_BENEFITS2 = 24;
|
|
4053
|
-
var
|
|
4378
|
+
var MAX_MARKDOWN4 = 65536;
|
|
4054
4379
|
var MAX_LABEL2 = 160;
|
|
4055
4380
|
var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
|
|
4056
4381
|
var MARKDOWN_HEADING = /^#{1,6}\s+/;
|
|
@@ -4075,17 +4400,17 @@ function markdownBenefits(lines, path, errors) {
|
|
|
4075
4400
|
errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
|
|
4076
4401
|
continue;
|
|
4077
4402
|
}
|
|
4078
|
-
const
|
|
4079
|
-
if (
|
|
4080
|
-
if (
|
|
4403
|
+
const text4 = (match[1] ?? "").trim();
|
|
4404
|
+
if (text4.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
|
|
4405
|
+
if (text4) benefits.push(text4);
|
|
4081
4406
|
}
|
|
4082
4407
|
if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
|
|
4083
4408
|
return benefits.slice(0, MAX_BENEFITS2);
|
|
4084
4409
|
}
|
|
4085
4410
|
function applyMembershipTierMarkdown(presentation, source) {
|
|
4086
4411
|
if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
|
|
4087
|
-
if (source.length >
|
|
4088
|
-
return { ok: false, errors: [`membership Markdown must be at most ${
|
|
4412
|
+
if (source.length > MAX_MARKDOWN4) {
|
|
4413
|
+
return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN4.toLocaleString("en-US")} characters`] };
|
|
4089
4414
|
}
|
|
4090
4415
|
if (!source.trim()) return { ok: true, value: { ...presentation } };
|
|
4091
4416
|
const errors = [];
|
|
@@ -4263,6 +4588,11 @@ async function exportChapterPromotion(input) {
|
|
|
4263
4588
|
export {
|
|
4264
4589
|
ADMISSION_GRANTS_PATH,
|
|
4265
4590
|
ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
4591
|
+
BUILDER_LIBRARY_ADMIN_PATH,
|
|
4592
|
+
BUILDER_LIBRARY_CONTROL_PATH,
|
|
4593
|
+
BUILDER_LIBRARY_MANIFEST_PATH,
|
|
4594
|
+
BUILDER_LIBRARY_PROMOTE_PATH,
|
|
4595
|
+
BUILDER_LIBRARY_SCHEMA_VERSION,
|
|
4266
4596
|
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
4267
4597
|
CHAPTER_PROMOTION_ADAPTER,
|
|
4268
4598
|
DEFAULT_CHAPTER_COPY,
|
|
@@ -4287,13 +4617,17 @@ export {
|
|
|
4287
4617
|
bookingDecision,
|
|
4288
4618
|
brandTokens,
|
|
4289
4619
|
bucketSeries,
|
|
4620
|
+
buildBuilderLibraryManifest,
|
|
4290
4621
|
buildGroupSeed,
|
|
4291
4622
|
buildTierSeeds,
|
|
4623
|
+
builderLibraryDigest,
|
|
4292
4624
|
canApprove,
|
|
4293
4625
|
canBook,
|
|
4294
4626
|
canChangeRole,
|
|
4295
4627
|
canTransition,
|
|
4628
|
+
canViewBuilderLibraryResource,
|
|
4296
4629
|
canceledPatch,
|
|
4630
|
+
canonicalBuilderLibrary,
|
|
4297
4631
|
canonicalMemberContent,
|
|
4298
4632
|
canonicalSignupControl,
|
|
4299
4633
|
chapterBrandFromTokens,
|
|
@@ -4309,6 +4643,7 @@ export {
|
|
|
4309
4643
|
clerkUserRequest,
|
|
4310
4644
|
composeMemberContent,
|
|
4311
4645
|
createAdmissionGrantEnvelope,
|
|
4646
|
+
createBuilderLibraryEnvelope,
|
|
4312
4647
|
createChapterIntegration,
|
|
4313
4648
|
createClerkInvitation,
|
|
4314
4649
|
createClerkUser,
|
|
@@ -4364,8 +4699,10 @@ export {
|
|
|
4364
4699
|
personInputFromApp,
|
|
4365
4700
|
planDelivery,
|
|
4366
4701
|
projectApplicant,
|
|
4702
|
+
projectBuilderLibraryDelivery,
|
|
4367
4703
|
projectMemberContent,
|
|
4368
4704
|
projectSharedRecord,
|
|
4705
|
+
promoteBuilderLibraryResource,
|
|
4369
4706
|
publicGiftOffers,
|
|
4370
4707
|
reconcileMeetings,
|
|
4371
4708
|
refundedPatch,
|
|
@@ -4376,6 +4713,7 @@ export {
|
|
|
4376
4713
|
renewalPatch,
|
|
4377
4714
|
resolveApplication,
|
|
4378
4715
|
resolveAuth,
|
|
4716
|
+
resolveBuilderLibrary,
|
|
4379
4717
|
resolveChapterCopy,
|
|
4380
4718
|
resolveJoinContinuation,
|
|
4381
4719
|
resolveLeaderFormation,
|
|
@@ -4402,6 +4740,7 @@ export {
|
|
|
4402
4740
|
updateClerkUserMetadata,
|
|
4403
4741
|
updateClerkUserMetadataByEmail,
|
|
4404
4742
|
validateAdmissionGrantDocument,
|
|
4743
|
+
validateBuilderLibraryDocument,
|
|
4405
4744
|
validateGiftOfferCatalog,
|
|
4406
4745
|
validateMemberContentDocument,
|
|
4407
4746
|
validateMembershipPageDocument,
|