@odla-ai/chapter 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/dist/{chunk-I45VYBUV.js → chunk-BIYWPPH6.js} +233 -140
- package/dist/chunk-BIYWPPH6.js.map +1 -0
- package/dist/{chunk-LWM32REV.js → chunk-SI3JHSEX.js} +211 -64
- package/dist/chunk-SI3JHSEX.js.map +1 -0
- package/dist/{copy-context-0k0-yWHL.d.ts → copy-context-Pbs3keLx.d.ts} +174 -28
- package/dist/index.cjs +483 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +241 -29
- package/dist/index.d.ts +241 -29
- package/dist/index.js +483 -97
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +27 -3
- package/dist/ui/admin/index.js +5 -1
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +8 -2
- package/dist/ui/member/index.d.ts +28 -3
- package/dist/ui/member/index.js +3 -1
- package/dist/worker/index.cjs +736 -126
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +151 -28
- package/dist/worker/index.d.ts +151 -28
- package/dist/worker/index.js +729 -119
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/greenfield.md +27 -0
- package/dist/chunk-I45VYBUV.js.map +0 -1
- package/dist/chunk-LWM32REV.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,9 @@ __export(index_exports, {
|
|
|
28
28
|
GIFT_OFFER_CATALOG_VERSION: () => GIFT_OFFER_CATALOG_VERSION,
|
|
29
29
|
MAX_GIFT_OFFERS: () => MAX_GIFT_OFFERS,
|
|
30
30
|
MEMBERSHIP_PAGE_RENDERER: () => MEMBERSHIP_PAGE_RENDERER,
|
|
31
|
+
MEMBER_CONTENT_CONTROL_PATH: () => MEMBER_CONTENT_CONTROL_PATH,
|
|
32
|
+
MEMBER_CONTENT_PATH: () => MEMBER_CONTENT_PATH,
|
|
33
|
+
MEMBER_CONTENT_SCHEMA_VERSION: () => MEMBER_CONTENT_SCHEMA_VERSION,
|
|
31
34
|
SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
|
|
32
35
|
SIGNUP_CONTROL_PATH: () => SIGNUP_CONTROL_PATH,
|
|
33
36
|
SIGNUP_CONTROL_SCHEMA_VERSION: () => SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
@@ -48,6 +51,7 @@ __export(index_exports, {
|
|
|
48
51
|
canChangeRole: () => canChangeRole,
|
|
49
52
|
canTransition: () => canTransition,
|
|
50
53
|
canceledPatch: () => canceledPatch,
|
|
54
|
+
canonicalMemberContent: () => canonicalMemberContent,
|
|
51
55
|
canonicalSignupControl: () => canonicalSignupControl,
|
|
52
56
|
chapterBrandFromTokens: () => chapterBrandFromTokens,
|
|
53
57
|
chapterDb: () => chapterDb,
|
|
@@ -59,10 +63,12 @@ __export(index_exports, {
|
|
|
59
63
|
clerkListUsers: () => clerkListUsers,
|
|
60
64
|
clerkSetRole: () => clerkSetRole,
|
|
61
65
|
clerkUserRequest: () => clerkUserRequest,
|
|
66
|
+
composeMemberContent: () => composeMemberContent,
|
|
62
67
|
createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
|
|
63
68
|
createChapterIntegration: () => createChapterIntegration,
|
|
64
69
|
createClerkInvitation: () => createClerkInvitation,
|
|
65
70
|
createClerkUser: () => createClerkUser,
|
|
71
|
+
createMemberContentEnvelope: () => createMemberContentEnvelope,
|
|
66
72
|
createSignupControlEnvelope: () => createSignupControlEnvelope,
|
|
67
73
|
dashboardMetricData: () => dashboardMetricData,
|
|
68
74
|
defaultCrm: () => defaultCrm,
|
|
@@ -98,6 +104,8 @@ __export(index_exports, {
|
|
|
98
104
|
meetingCreateRow: () => meetingCreateRow,
|
|
99
105
|
meetingRescheduleUpdate: () => meetingRescheduleUpdate,
|
|
100
106
|
memberApplication: () => memberApplication,
|
|
107
|
+
memberContentDigest: () => memberContentDigest,
|
|
108
|
+
memberContentDocument: () => memberContentDocument,
|
|
101
109
|
memberSession: () => memberSession,
|
|
102
110
|
membershipPageDocument: () => membershipPageDocument,
|
|
103
111
|
membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
|
|
@@ -110,6 +118,7 @@ __export(index_exports, {
|
|
|
110
118
|
personInputFromApp: () => personInputFromApp,
|
|
111
119
|
planDelivery: () => planDelivery,
|
|
112
120
|
projectApplicant: () => projectApplicant,
|
|
121
|
+
projectMemberContent: () => projectMemberContent,
|
|
113
122
|
projectSharedRecord: () => projectSharedRecord,
|
|
114
123
|
publicGiftOffers: () => publicGiftOffers,
|
|
115
124
|
reconcileMeetings: () => reconcileMeetings,
|
|
@@ -124,6 +133,7 @@ __export(index_exports, {
|
|
|
124
133
|
resolveChapterCopy: () => resolveChapterCopy,
|
|
125
134
|
resolveJoinContinuation: () => resolveJoinContinuation,
|
|
126
135
|
resolveLeaderFormation: () => resolveLeaderFormation,
|
|
136
|
+
resolveMemberContent: () => resolveMemberContent,
|
|
127
137
|
resolvePipeline: () => resolvePipeline,
|
|
128
138
|
resolveScheduling: () => resolveScheduling,
|
|
129
139
|
resolveSignupControl: () => resolveSignupControl,
|
|
@@ -147,6 +157,7 @@ __export(index_exports, {
|
|
|
147
157
|
updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
|
|
148
158
|
validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
|
|
149
159
|
validateGiftOfferCatalog: () => validateGiftOfferCatalog,
|
|
160
|
+
validateMemberContentDocument: () => validateMemberContentDocument,
|
|
150
161
|
validateMembershipPageDocument: () => validateMembershipPageDocument,
|
|
151
162
|
validateScheduling: () => validateScheduling,
|
|
152
163
|
validateSignupControlDocument: () => validateSignupControlDocument,
|
|
@@ -184,9 +195,9 @@ function canonicalValue(value) {
|
|
|
184
195
|
}
|
|
185
196
|
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
186
197
|
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
187
|
-
const
|
|
188
|
-
const entries = Object.keys(
|
|
189
|
-
const item =
|
|
198
|
+
const record3 = value;
|
|
199
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
200
|
+
const item = record3[key];
|
|
190
201
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
191
202
|
});
|
|
192
203
|
return `{${entries.join(",")}}`;
|
|
@@ -444,14 +455,14 @@ function hasDisclaimerAck(fields) {
|
|
|
444
455
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
445
456
|
function applicantProfile(chapter, fields) {
|
|
446
457
|
const app = chapter.application;
|
|
447
|
-
const
|
|
458
|
+
const allowed2 = (f) => app.profileFields.includes(f);
|
|
448
459
|
const profile = {};
|
|
449
460
|
for (const f of [...app.required, ...app.optional]) {
|
|
450
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
461
|
+
if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
|
|
451
462
|
const v = fields[f];
|
|
452
463
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
453
464
|
}
|
|
454
|
-
if (fields.focus !== void 0 &&
|
|
465
|
+
if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
455
466
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
456
467
|
}
|
|
457
468
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -806,6 +817,30 @@ var signupControls = {
|
|
|
806
817
|
receivedAt: attr4("number", { indexed: true })
|
|
807
818
|
}
|
|
808
819
|
};
|
|
820
|
+
var memberContentControls = {
|
|
821
|
+
attrs: {
|
|
822
|
+
id: id3(),
|
|
823
|
+
sourceId: attr4("string", { indexed: true }),
|
|
824
|
+
targetId: attr4("string", { indexed: true }),
|
|
825
|
+
environment: attr4("string", { indexed: true }),
|
|
826
|
+
revision: attr4("number", { indexed: true }),
|
|
827
|
+
digest: attr4("string", { indexed: true }),
|
|
828
|
+
document: attr4("json"),
|
|
829
|
+
active: attr4("boolean", { indexed: true }),
|
|
830
|
+
receivedAt: attr4("number", { indexed: true })
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
var memberContentLocal = {
|
|
834
|
+
attrs: {
|
|
835
|
+
id: id3(),
|
|
836
|
+
chapterId: attr4("string", { indexed: true }),
|
|
837
|
+
environment: attr4("string", { indexed: true }),
|
|
838
|
+
revision: attr4("number", { indexed: true }),
|
|
839
|
+
document: attr4("json"),
|
|
840
|
+
updatedAt: attr4("number", { indexed: true }),
|
|
841
|
+
updatedBy: attr4("string", { optional: true })
|
|
842
|
+
}
|
|
843
|
+
};
|
|
809
844
|
var groups = {
|
|
810
845
|
attrs: {
|
|
811
846
|
id: id3(),
|
|
@@ -877,7 +912,7 @@ var emailLog = {
|
|
|
877
912
|
sentAt: attr4("number", { indexed: true })
|
|
878
913
|
}
|
|
879
914
|
};
|
|
880
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
915
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
|
|
881
916
|
const entities = {};
|
|
882
917
|
if (mode === "chapter") {
|
|
883
918
|
entities.applications = applicationsFor(application);
|
|
@@ -893,6 +928,10 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
893
928
|
entities.giftEntitlements = giftEntitlementsEntity;
|
|
894
929
|
entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
|
|
895
930
|
if (includeSignupControl) entities.signupControls = signupControls;
|
|
931
|
+
if (includeMemberContent) {
|
|
932
|
+
entities.memberContentControls = memberContentControls;
|
|
933
|
+
entities.memberContentLocal = memberContentLocal;
|
|
934
|
+
}
|
|
896
935
|
}
|
|
897
936
|
if (auth.source === "table") entities.admins = admins;
|
|
898
937
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
@@ -1738,15 +1777,35 @@ function resolveSignupControl(input) {
|
|
|
1738
1777
|
return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
|
|
1739
1778
|
}
|
|
1740
1779
|
|
|
1741
|
-
// src/config.ts
|
|
1780
|
+
// src/config-member-content.ts
|
|
1742
1781
|
var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1782
|
+
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1783
|
+
function resolveMemberContent(input) {
|
|
1784
|
+
if (!input) return null;
|
|
1785
|
+
const parent = input.parent;
|
|
1786
|
+
if (parent && !SLUG3.test(parent.sourceId)) {
|
|
1787
|
+
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1788
|
+
}
|
|
1789
|
+
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
1790
|
+
if (parent && !SECRET_NAME2.test(secretName)) {
|
|
1791
|
+
throw new Error("defineChapter.memberContent.parent.secretName: must be a vault-key identifier");
|
|
1792
|
+
}
|
|
1793
|
+
return {
|
|
1794
|
+
enabled: true,
|
|
1795
|
+
localAuthoring: input.localAuthoring !== false,
|
|
1796
|
+
parent: parent ? { sourceId: parent.sourceId, secretName } : null
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// src/config.ts
|
|
1801
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1743
1802
|
function isResolvedCrm(x) {
|
|
1744
1803
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1745
1804
|
}
|
|
1746
1805
|
function defineChapter(config) {
|
|
1747
1806
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1748
1807
|
const { id: id4, name } = config;
|
|
1749
|
-
if (typeof id4 !== "string" || !
|
|
1808
|
+
if (typeof id4 !== "string" || !SLUG4.test(id4)) {
|
|
1750
1809
|
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1751
1810
|
}
|
|
1752
1811
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
@@ -1771,16 +1830,21 @@ function defineChapter(config) {
|
|
|
1771
1830
|
const copy = resolveChapterCopy(config.copy);
|
|
1772
1831
|
const network = resolveNetwork(config, crm);
|
|
1773
1832
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1833
|
+
const memberContent = resolveMemberContent(config.memberContent);
|
|
1774
1834
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1775
1835
|
if (signupControl && mode !== "chapter") {
|
|
1776
1836
|
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
1777
1837
|
}
|
|
1838
|
+
if (memberContent && mode !== "chapter") {
|
|
1839
|
+
throw new Error("defineChapter.memberContent: only chapter-mode sites can serve member content");
|
|
1840
|
+
}
|
|
1778
1841
|
const { schema, rules } = chapterDb(
|
|
1779
1842
|
mode,
|
|
1780
1843
|
auth,
|
|
1781
1844
|
network.targets.length > 0,
|
|
1782
1845
|
application,
|
|
1783
|
-
Boolean(signupControl)
|
|
1846
|
+
Boolean(signupControl),
|
|
1847
|
+
Boolean(memberContent)
|
|
1784
1848
|
);
|
|
1785
1849
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1786
1850
|
const account = config.account ?? "none";
|
|
@@ -1837,6 +1901,7 @@ function defineChapter(config) {
|
|
|
1837
1901
|
copy,
|
|
1838
1902
|
network,
|
|
1839
1903
|
signupControl,
|
|
1904
|
+
memberContent,
|
|
1840
1905
|
formation,
|
|
1841
1906
|
crm,
|
|
1842
1907
|
auth,
|
|
@@ -2131,12 +2196,332 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2131
2196
|
}] : [],
|
|
2132
2197
|
// Formation metadata is public only when the host explicitly enables
|
|
2133
2198
|
// the bounded intake contract.
|
|
2134
|
-
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
2199
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
|
|
2200
|
+
...chapter.memberContent ? [
|
|
2201
|
+
{ path: "/api/member-content", expectedStatus: 401 },
|
|
2202
|
+
{ path: "/api/admin/member-content", expectedStatus: 401 }
|
|
2203
|
+
] : []
|
|
2135
2204
|
],
|
|
2136
2205
|
runbooks
|
|
2137
2206
|
};
|
|
2138
2207
|
}
|
|
2139
2208
|
|
|
2209
|
+
// src/member-content-validation-helpers.ts
|
|
2210
|
+
var ID2 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2211
|
+
var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2212
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2213
|
+
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2214
|
+
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
2215
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
2216
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
2217
|
+
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
2218
|
+
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
2219
|
+
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
2220
|
+
var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
|
|
2221
|
+
var MAX_PAGES = 100;
|
|
2222
|
+
var MAX_MARKDOWN = 65536;
|
|
2223
|
+
var MAX_TEXT = 240;
|
|
2224
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2225
|
+
function unknownKeys(value, allowed2, path, errors) {
|
|
2226
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2227
|
+
}
|
|
2228
|
+
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
2229
|
+
if (value === void 0) {
|
|
2230
|
+
if (required) errors.push(`${path} is required`);
|
|
2231
|
+
return void 0;
|
|
2232
|
+
}
|
|
2233
|
+
if (typeof value !== "string") {
|
|
2234
|
+
errors.push(`${path} must be a string`);
|
|
2235
|
+
return void 0;
|
|
2236
|
+
}
|
|
2237
|
+
const valueText = value.trim();
|
|
2238
|
+
if (required && !valueText) errors.push(`${path} is required`);
|
|
2239
|
+
if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2240
|
+
return valueText;
|
|
2241
|
+
}
|
|
2242
|
+
function offerValue(value, path, errors, tiers2, audience) {
|
|
2243
|
+
const raw = record(value);
|
|
2244
|
+
if (!raw) {
|
|
2245
|
+
errors.push(`${path} must be an object`);
|
|
2246
|
+
return void 0;
|
|
2247
|
+
}
|
|
2248
|
+
unknownKeys(raw, OFFER_KEYS, path, errors);
|
|
2249
|
+
const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2250
|
+
if (tierId && !ID2.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2251
|
+
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2252
|
+
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2253
|
+
errors.push(`${path}.tierId must grant access to this page`);
|
|
2254
|
+
}
|
|
2255
|
+
const label = textValue(raw.label, `${path}.label`, errors);
|
|
2256
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
|
|
2257
|
+
const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2258
|
+
return {
|
|
2259
|
+
tierId,
|
|
2260
|
+
...label ? { label } : {},
|
|
2261
|
+
...bodyMarkdown ? { bodyMarkdown } : {},
|
|
2262
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/member-content-validation.ts
|
|
2267
|
+
function validateMemberContentDocument(input, options = {}) {
|
|
2268
|
+
const errors = [];
|
|
2269
|
+
const root = record(input);
|
|
2270
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2271
|
+
unknownKeys(root, ROOT_KEYS, "document", errors);
|
|
2272
|
+
if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2273
|
+
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
2274
|
+
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
2275
|
+
const sourceRaw = record(root.source);
|
|
2276
|
+
let source = { type: "chapter", appId: "invalid" };
|
|
2277
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2278
|
+
else {
|
|
2279
|
+
unknownKeys(sourceRaw, SOURCE_KEYS, "document.source", errors);
|
|
2280
|
+
const type = sourceRaw.type;
|
|
2281
|
+
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
2282
|
+
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
2283
|
+
if (appId && !ID2.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2284
|
+
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2285
|
+
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2286
|
+
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
2287
|
+
}
|
|
2288
|
+
let target;
|
|
2289
|
+
if (root.target !== void 0) {
|
|
2290
|
+
const targetRaw = record(root.target);
|
|
2291
|
+
if (!targetRaw) errors.push("document.target must be an object");
|
|
2292
|
+
else {
|
|
2293
|
+
unknownKeys(targetRaw, TARGET_KEYS, "document.target", errors);
|
|
2294
|
+
const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
|
|
2295
|
+
const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
|
|
2296
|
+
target = { chapterId, environment };
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
|
|
2300
|
+
if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
|
|
2301
|
+
if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
|
|
2302
|
+
const rawPages = Array.isArray(root.pages) ? root.pages : [];
|
|
2303
|
+
if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
|
|
2304
|
+
const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
|
|
2305
|
+
const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
|
|
2306
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2307
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2308
|
+
const pages = [];
|
|
2309
|
+
for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2310
|
+
const path = `document.pages[${index}]`;
|
|
2311
|
+
const raw = record(value);
|
|
2312
|
+
if (!raw) {
|
|
2313
|
+
errors.push(`${path} must be an object`);
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
unknownKeys(raw, PAGE_KEYS, path, errors);
|
|
2317
|
+
const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2318
|
+
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2319
|
+
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2320
|
+
if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2321
|
+
if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2322
|
+
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2323
|
+
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2324
|
+
ids.add(id4);
|
|
2325
|
+
slugs.add(slug);
|
|
2326
|
+
const navRaw = record(raw.navigation);
|
|
2327
|
+
let navigation = { section: "General", order: index };
|
|
2328
|
+
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2329
|
+
else {
|
|
2330
|
+
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2331
|
+
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2332
|
+
const order = navRaw.order;
|
|
2333
|
+
if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
|
|
2334
|
+
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2335
|
+
}
|
|
2336
|
+
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2337
|
+
navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
|
|
2338
|
+
}
|
|
2339
|
+
const audienceRaw = record(raw.audience);
|
|
2340
|
+
let audience = { kind: "all-members" };
|
|
2341
|
+
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2342
|
+
else {
|
|
2343
|
+
unknownKeys(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
|
|
2344
|
+
if (audienceRaw.kind === "all-members") {
|
|
2345
|
+
if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
|
|
2346
|
+
} else if (audienceRaw.kind === "tiers") {
|
|
2347
|
+
if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
|
|
2348
|
+
errors.push(`${path}.audience.tierIds must be a non-empty array`);
|
|
2349
|
+
}
|
|
2350
|
+
const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
|
|
2351
|
+
const normalized = [];
|
|
2352
|
+
for (const [tierIndex, value2] of values.entries()) {
|
|
2353
|
+
const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
|
|
2354
|
+
if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
|
|
2355
|
+
if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
|
|
2356
|
+
normalized.push(tier);
|
|
2357
|
+
}
|
|
2358
|
+
audience = { kind: "tiers", tierIds: normalized };
|
|
2359
|
+
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2360
|
+
}
|
|
2361
|
+
const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
|
|
2362
|
+
const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
|
|
2363
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
|
|
2364
|
+
let upsell;
|
|
2365
|
+
if (raw.upsell !== void 0) {
|
|
2366
|
+
const upsellRaw = record(raw.upsell);
|
|
2367
|
+
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2368
|
+
else {
|
|
2369
|
+
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
2370
|
+
const owner = upsellRaw.owner;
|
|
2371
|
+
if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
|
|
2372
|
+
if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
|
|
2373
|
+
if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
|
|
2374
|
+
if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
|
|
2375
|
+
const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
|
|
2376
|
+
if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
|
|
2377
|
+
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
pages.push({ id: id4, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2381
|
+
}
|
|
2382
|
+
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2383
|
+
for (const [index, page] of pages.entries()) {
|
|
2384
|
+
const parentId = page.navigation.parentId;
|
|
2385
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
|
|
2386
|
+
if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
|
|
2387
|
+
}
|
|
2388
|
+
for (const page of pages) {
|
|
2389
|
+
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
2390
|
+
let parentId = page.navigation.parentId;
|
|
2391
|
+
while (parentId) {
|
|
2392
|
+
if (seen.has(parentId)) {
|
|
2393
|
+
errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
seen.add(parentId);
|
|
2397
|
+
parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
let upsellOverrides;
|
|
2401
|
+
if (root.upsellOverrides !== void 0) {
|
|
2402
|
+
if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
|
|
2403
|
+
if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
|
|
2404
|
+
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2405
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2406
|
+
upsellOverrides = [];
|
|
2407
|
+
for (const [index, value] of values.entries()) {
|
|
2408
|
+
const path = `document.upsellOverrides[${index}]`;
|
|
2409
|
+
const raw = record(value);
|
|
2410
|
+
if (!raw) {
|
|
2411
|
+
errors.push(`${path} must be an object`);
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
unknownKeys(raw, OVERRIDE_KEYS, path, errors);
|
|
2415
|
+
const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2416
|
+
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2417
|
+
seen.add(pageId);
|
|
2418
|
+
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2419
|
+
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2420
|
+
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2421
|
+
const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2422
|
+
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
if (errors.length) return { ok: false, errors };
|
|
2426
|
+
return { ok: true, value: {
|
|
2427
|
+
schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
|
|
2428
|
+
revision: Number(root.revision),
|
|
2429
|
+
generatedAt: Number(root.generatedAt),
|
|
2430
|
+
source,
|
|
2431
|
+
...target ? { target } : {},
|
|
2432
|
+
pages,
|
|
2433
|
+
...upsellOverrides ? { upsellOverrides } : {}
|
|
2434
|
+
} };
|
|
2435
|
+
}
|
|
2436
|
+
function memberContentDocument(input, options = {}) {
|
|
2437
|
+
const result = validateMemberContentDocument(input, options);
|
|
2438
|
+
if (!result.ok) throw new TypeError(`Invalid member content document: ${result.errors.join("; ")}`);
|
|
2439
|
+
return result.value;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// src/member-content.ts
|
|
2443
|
+
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
2444
|
+
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
2445
|
+
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
2446
|
+
var pageOrder = (a, b) => a.navigation.section.localeCompare(b.navigation.section) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
|
|
2447
|
+
function composeMemberContent(parent, chapter) {
|
|
2448
|
+
const pages = [];
|
|
2449
|
+
const parentPages = /* @__PURE__ */ new Map();
|
|
2450
|
+
for (const page of parent?.pages ?? []) {
|
|
2451
|
+
const effective = { ...page, source: parent.source };
|
|
2452
|
+
parentPages.set(page.id, effective);
|
|
2453
|
+
pages.push(effective);
|
|
2454
|
+
}
|
|
2455
|
+
for (const override of chapter?.upsellOverrides ?? []) {
|
|
2456
|
+
const page = parentPages.get(override.pageId);
|
|
2457
|
+
if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
|
|
2458
|
+
}
|
|
2459
|
+
for (const page of chapter?.pages ?? []) {
|
|
2460
|
+
if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
|
|
2461
|
+
throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
|
|
2462
|
+
}
|
|
2463
|
+
pages.push({ ...page, source: chapter.source });
|
|
2464
|
+
}
|
|
2465
|
+
return {
|
|
2466
|
+
pages: pages.sort(pageOrder),
|
|
2467
|
+
parentRevision: parent?.revision ?? null,
|
|
2468
|
+
chapterRevision: chapter?.revision ?? null
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
2472
|
+
function projectMemberContent(content, viewer, tiers2) {
|
|
2473
|
+
const authority = new Map(tiers2.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
2474
|
+
return {
|
|
2475
|
+
viewer: { ...viewer },
|
|
2476
|
+
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
2477
|
+
pages: content.pages.map((page) => {
|
|
2478
|
+
const canRead = allowed(page, viewer);
|
|
2479
|
+
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
2480
|
+
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
2481
|
+
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
2482
|
+
return {
|
|
2483
|
+
id: page.id,
|
|
2484
|
+
slug: page.slug,
|
|
2485
|
+
title: page.title,
|
|
2486
|
+
...page.summary ? { summary: page.summary } : {},
|
|
2487
|
+
navigation: page.navigation,
|
|
2488
|
+
audience: page.audience,
|
|
2489
|
+
source: page.source,
|
|
2490
|
+
access: canRead ? "allowed" : "locked",
|
|
2491
|
+
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
2492
|
+
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
2493
|
+
...upsell ? { upsell } : {}
|
|
2494
|
+
};
|
|
2495
|
+
})
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// src/member-content-canonical.ts
|
|
2500
|
+
var encoder2 = new TextEncoder();
|
|
2501
|
+
function canonicalValue2(value) {
|
|
2502
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2503
|
+
return JSON.stringify(value);
|
|
2504
|
+
}
|
|
2505
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2506
|
+
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2507
|
+
const record3 = value;
|
|
2508
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2509
|
+
const item = record3[key];
|
|
2510
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2511
|
+
});
|
|
2512
|
+
return `{${entries.join(",")}}`;
|
|
2513
|
+
}
|
|
2514
|
+
function canonicalMemberContent(value) {
|
|
2515
|
+
return canonicalValue2(value);
|
|
2516
|
+
}
|
|
2517
|
+
async function memberContentDigest(document) {
|
|
2518
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
|
|
2519
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2520
|
+
}
|
|
2521
|
+
async function createMemberContentEnvelope(document) {
|
|
2522
|
+
return { document, digest: await memberContentDigest(document) };
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2140
2525
|
// src/email.ts
|
|
2141
2526
|
function render(template, vars) {
|
|
2142
2527
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -2331,10 +2716,10 @@ function resolveJoinContinuation(input) {
|
|
|
2331
2716
|
// src/gift-offers.ts
|
|
2332
2717
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2333
2718
|
var MAX_GIFT_OFFERS = 20;
|
|
2334
|
-
var
|
|
2719
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2335
2720
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2336
2721
|
var CURRENCY = /^[a-z]{3}$/;
|
|
2337
|
-
var
|
|
2722
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2338
2723
|
var text = (value, path, errors, max) => {
|
|
2339
2724
|
if (typeof value !== "string" || !value.trim()) {
|
|
2340
2725
|
errors.push(`${path} is required`);
|
|
@@ -2345,7 +2730,7 @@ var text = (value, path, errors, max) => {
|
|
|
2345
2730
|
};
|
|
2346
2731
|
function validateGiftOfferCatalog(input, tierIds) {
|
|
2347
2732
|
const errors = [];
|
|
2348
|
-
const root =
|
|
2733
|
+
const root = record2(input);
|
|
2349
2734
|
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2350
2735
|
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2351
2736
|
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
@@ -2356,18 +2741,18 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2356
2741
|
const offers = [];
|
|
2357
2742
|
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2358
2743
|
const path = `document.giftOffers.offers[${index}]`;
|
|
2359
|
-
const item =
|
|
2744
|
+
const item = record2(raw);
|
|
2360
2745
|
if (!item) {
|
|
2361
2746
|
errors.push(`${path} must be an object`);
|
|
2362
2747
|
continue;
|
|
2363
2748
|
}
|
|
2364
|
-
const
|
|
2365
|
-
for (const key of Object.keys(item)) if (!
|
|
2749
|
+
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2750
|
+
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2366
2751
|
const id4 = text(item.id, `${path}.id`, errors, 80);
|
|
2367
|
-
if (id4 && !
|
|
2752
|
+
if (id4 && !ID3.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2368
2753
|
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2369
2754
|
ids.add(id4);
|
|
2370
|
-
const targetRaw =
|
|
2755
|
+
const targetRaw = record2(item.target);
|
|
2371
2756
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2372
2757
|
if (targetRaw?.type === "tier") {
|
|
2373
2758
|
const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
@@ -2375,7 +2760,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2375
2760
|
target = { type: "tier", tierId };
|
|
2376
2761
|
} else if (targetRaw?.type === "entitlement") {
|
|
2377
2762
|
const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
2378
|
-
if (entitlementKey && !
|
|
2763
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2379
2764
|
target = { type: "entitlement", entitlementKey };
|
|
2380
2765
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2381
2766
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
@@ -2455,34 +2840,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
2455
2840
|
if (readable && raw.length <= 64) return raw;
|
|
2456
2841
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
2457
2842
|
}
|
|
2458
|
-
function normalizeSharedRecord(
|
|
2459
|
-
if ("input" in
|
|
2843
|
+
function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
2844
|
+
if ("input" in record3 && record3.version === 2) {
|
|
2460
2845
|
return {
|
|
2461
2846
|
version: 2,
|
|
2462
|
-
sourceId:
|
|
2463
|
-
sourceRecordId:
|
|
2464
|
-
type:
|
|
2465
|
-
input:
|
|
2847
|
+
sourceId: record3.source.siteId,
|
|
2848
|
+
sourceRecordId: record3.source.recordId,
|
|
2849
|
+
type: record3.type,
|
|
2850
|
+
input: record3.input
|
|
2466
2851
|
};
|
|
2467
2852
|
}
|
|
2468
|
-
if ("input" in
|
|
2853
|
+
if ("input" in record3) {
|
|
2469
2854
|
return {
|
|
2470
2855
|
version: 1,
|
|
2471
2856
|
sourceId: fallbackSourceId,
|
|
2472
|
-
sourceRecordId:
|
|
2473
|
-
type:
|
|
2474
|
-
input:
|
|
2857
|
+
sourceRecordId: record3.hubRecordId,
|
|
2858
|
+
type: record3.type,
|
|
2859
|
+
input: record3.input
|
|
2475
2860
|
};
|
|
2476
2861
|
}
|
|
2477
|
-
if ("type" in
|
|
2478
|
-
const input = { name:
|
|
2862
|
+
if ("type" in record3 && record3.type === "company") {
|
|
2863
|
+
const input = { name: record3.name };
|
|
2479
2864
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
2480
|
-
if (
|
|
2865
|
+
if (record3[key]) input[key] = record3[key];
|
|
2481
2866
|
}
|
|
2482
2867
|
return {
|
|
2483
2868
|
version: 1,
|
|
2484
2869
|
sourceId: fallbackSourceId,
|
|
2485
|
-
sourceRecordId:
|
|
2870
|
+
sourceRecordId: record3.hubRecordId,
|
|
2486
2871
|
type: "company",
|
|
2487
2872
|
input
|
|
2488
2873
|
};
|
|
@@ -2490,28 +2875,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
|
|
|
2490
2875
|
return {
|
|
2491
2876
|
version: 1,
|
|
2492
2877
|
sourceId: fallbackSourceId,
|
|
2493
|
-
sourceRecordId:
|
|
2878
|
+
sourceRecordId: record3.hubRecordId,
|
|
2494
2879
|
type: "person",
|
|
2495
|
-
input: sharedPersonInput(
|
|
2880
|
+
input: sharedPersonInput(record3)
|
|
2496
2881
|
};
|
|
2497
2882
|
}
|
|
2498
|
-
function sharedRecordFromCrm(crm,
|
|
2499
|
-
if (target.fields && !target.fields[
|
|
2500
|
-
throw new Error(`${target.name} does not accept "${
|
|
2883
|
+
function sharedRecordFromCrm(crm, record3, target, sourceId) {
|
|
2884
|
+
if (target.fields && !target.fields[record3.type]) {
|
|
2885
|
+
throw new Error(`${target.name} does not accept "${record3.type}" records`);
|
|
2501
2886
|
}
|
|
2502
|
-
const def = crm.type(
|
|
2503
|
-
const fields = target.fields?.[
|
|
2887
|
+
const def = crm.type(record3.type);
|
|
2888
|
+
const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
|
|
2504
2889
|
if (!fields) {
|
|
2505
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
2890
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
|
|
2506
2891
|
}
|
|
2507
2892
|
const nameField = def.nameField ?? "name";
|
|
2508
2893
|
const input = {};
|
|
2509
2894
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
2510
|
-
const value =
|
|
2895
|
+
const value = record3.fields?.[field];
|
|
2511
2896
|
if (value !== void 0) input[field] = value;
|
|
2512
2897
|
}
|
|
2513
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
2514
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
2898
|
+
if (input[nameField] === void 0) input[nameField] = record3.name;
|
|
2899
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
|
|
2515
2900
|
}
|
|
2516
2901
|
async function upsertPerson(deps, opts) {
|
|
2517
2902
|
const email = opts.email.toLowerCase();
|
|
@@ -2525,11 +2910,11 @@ async function upsertPerson(deps, opts) {
|
|
|
2525
2910
|
const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
2526
2911
|
return { recordId: created.id };
|
|
2527
2912
|
}
|
|
2528
|
-
async function findSharedRecord(deps,
|
|
2913
|
+
async function findSharedRecord(deps, record3, tag) {
|
|
2529
2914
|
const structured = await (0, import_crm5.getRecordByOrigin)(
|
|
2530
2915
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
2531
|
-
|
|
2532
|
-
|
|
2916
|
+
record3.sourceId,
|
|
2917
|
+
record3.sourceRecordId
|
|
2533
2918
|
);
|
|
2534
2919
|
if (structured) return structured;
|
|
2535
2920
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -2538,53 +2923,53 @@ async function findSharedRecord(deps, record2, tag) {
|
|
|
2538
2923
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
2539
2924
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2540
2925
|
}
|
|
2541
|
-
const def = deps.crm.type(
|
|
2926
|
+
const def = deps.crm.type(record3.type);
|
|
2542
2927
|
const emailField = def.emailField;
|
|
2543
|
-
if (emailField && typeof
|
|
2544
|
-
const primaryEmail =
|
|
2928
|
+
if (emailField && typeof record3.input[emailField] === "string") {
|
|
2929
|
+
const primaryEmail = record3.input[emailField].toLowerCase();
|
|
2545
2930
|
const found = await deps.db.query({
|
|
2546
|
-
crm_record: { $: { where: { type:
|
|
2931
|
+
crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
|
|
2547
2932
|
});
|
|
2548
2933
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2549
2934
|
}
|
|
2550
|
-
const domain =
|
|
2935
|
+
const domain = record3.input.domain;
|
|
2551
2936
|
const domainSlot = def.fields.domain?.slot;
|
|
2552
2937
|
if (typeof domain === "string" && domainSlot) {
|
|
2553
2938
|
const found = await deps.db.query({
|
|
2554
|
-
crm_record: { $: { where: { type:
|
|
2939
|
+
crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
|
|
2555
2940
|
});
|
|
2556
2941
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2557
2942
|
}
|
|
2558
2943
|
const nameField = def.nameField ?? "name";
|
|
2559
|
-
const name =
|
|
2560
|
-
if (
|
|
2944
|
+
const name = record3.input[nameField];
|
|
2945
|
+
if (record3.type === "company" && typeof name === "string" && name.trim()) {
|
|
2561
2946
|
const found = await deps.db.query({
|
|
2562
|
-
crm_record: { $: { where: { type:
|
|
2947
|
+
crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
|
|
2563
2948
|
});
|
|
2564
2949
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2565
2950
|
}
|
|
2566
2951
|
return void 0;
|
|
2567
2952
|
}
|
|
2568
2953
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
2569
|
-
const
|
|
2570
|
-
if (!
|
|
2954
|
+
const record3 = normalizeSharedRecord(shared, options.sourceId);
|
|
2955
|
+
if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
|
|
2571
2956
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
2572
2957
|
}
|
|
2573
|
-
if (options.sourceId && options.sourceId !==
|
|
2958
|
+
if (options.sourceId && options.sourceId !== record3.sourceId) {
|
|
2574
2959
|
throw new Error("signed sender does not match payload source");
|
|
2575
2960
|
}
|
|
2576
|
-
const tag = networkSourceTag(
|
|
2961
|
+
const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
|
|
2577
2962
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
2578
|
-
const existing = await findSharedRecord(deps,
|
|
2963
|
+
const existing = await findSharedRecord(deps, record3, tag);
|
|
2579
2964
|
let recordId;
|
|
2580
2965
|
if (existing && typeof existing.id === "string") {
|
|
2581
|
-
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input:
|
|
2966
|
+
await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record3.input });
|
|
2582
2967
|
recordId = existing.id;
|
|
2583
2968
|
} else {
|
|
2584
|
-
recordId = `network_${shortHash(`${
|
|
2969
|
+
recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
|
|
2585
2970
|
await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
|
|
2586
|
-
type:
|
|
2587
|
-
input:
|
|
2971
|
+
type: record3.type,
|
|
2972
|
+
input: record3.input,
|
|
2588
2973
|
mutationId: `share-create:${tag}`
|
|
2589
2974
|
});
|
|
2590
2975
|
}
|
|
@@ -2594,10 +2979,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
2594
2979
|
);
|
|
2595
2980
|
await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
|
|
2596
2981
|
recordId,
|
|
2597
|
-
sourceId:
|
|
2598
|
-
sourceRecordId:
|
|
2982
|
+
sourceId: record3.sourceId,
|
|
2983
|
+
sourceRecordId: record3.sourceRecordId,
|
|
2599
2984
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
2600
|
-
payloadVersion:
|
|
2985
|
+
payloadVersion: record3.version
|
|
2601
2986
|
});
|
|
2602
2987
|
return { recordId };
|
|
2603
2988
|
}
|
|
@@ -2881,8 +3266,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
2881
3266
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
2882
3267
|
const page = Array.isArray(data) ? data : [];
|
|
2883
3268
|
for (const u of page) {
|
|
2884
|
-
const
|
|
2885
|
-
if (
|
|
3269
|
+
const record3 = toRecord(u);
|
|
3270
|
+
if (record3) out.push(record3);
|
|
2886
3271
|
}
|
|
2887
3272
|
if (page.length < PAGE) break;
|
|
2888
3273
|
}
|
|
@@ -2984,7 +3369,8 @@ function applicationSummary(app) {
|
|
|
2984
3369
|
meetingLink: app.meetingLink ?? null,
|
|
2985
3370
|
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
2986
3371
|
renewalAt: app.renewalAt ?? null,
|
|
2987
|
-
canceled: app.canceled === true
|
|
3372
|
+
canceled: app.canceled === true,
|
|
3373
|
+
tierId: app.tierId ?? null
|
|
2988
3374
|
};
|
|
2989
3375
|
}
|
|
2990
3376
|
function memberApplication(app, meeting, defaultTimezone) {
|
|
@@ -3288,7 +3674,7 @@ var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
|
3288
3674
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
3289
3675
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3290
3676
|
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
3291
|
-
var
|
|
3677
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
3292
3678
|
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
3293
3679
|
"tierId",
|
|
3294
3680
|
"variant",
|
|
@@ -3307,12 +3693,12 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
|
3307
3693
|
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
3308
3694
|
var MAX_TIERS = 20;
|
|
3309
3695
|
var MAX_BENEFITS = 24;
|
|
3310
|
-
var
|
|
3696
|
+
var MAX_MARKDOWN2 = 65536;
|
|
3311
3697
|
var MAX_LABEL = 160;
|
|
3312
3698
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3313
|
-
function rejectUnknownKeys(value,
|
|
3699
|
+
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
3314
3700
|
for (const key of Object.keys(value)) {
|
|
3315
|
-
if (!
|
|
3701
|
+
if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3316
3702
|
}
|
|
3317
3703
|
}
|
|
3318
3704
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -3333,7 +3719,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3333
3719
|
const errors = [];
|
|
3334
3720
|
const root = objectRecord(input);
|
|
3335
3721
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3336
|
-
rejectUnknownKeys(root,
|
|
3722
|
+
rejectUnknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
3337
3723
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3338
3724
|
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
3339
3725
|
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
@@ -3387,7 +3773,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3387
3773
|
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
3388
3774
|
else {
|
|
3389
3775
|
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
3390
|
-
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors,
|
|
3776
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN2, true);
|
|
3391
3777
|
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
3392
3778
|
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
3393
3779
|
}
|
|
@@ -3409,10 +3795,10 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3409
3795
|
}
|
|
3410
3796
|
}
|
|
3411
3797
|
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
3412
|
-
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors,
|
|
3798
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN2);
|
|
3413
3799
|
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
3414
3800
|
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
3415
|
-
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors,
|
|
3801
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN2);
|
|
3416
3802
|
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
3417
3803
|
tiers2.push({
|
|
3418
3804
|
tierId,
|
|
@@ -3434,7 +3820,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3434
3820
|
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
3435
3821
|
}
|
|
3436
3822
|
}
|
|
3437
|
-
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors,
|
|
3823
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN2);
|
|
3438
3824
|
if (errors.length) return { ok: false, errors };
|
|
3439
3825
|
return {
|
|
3440
3826
|
ok: true,
|
|
@@ -3453,11 +3839,11 @@ function membershipPageDocument(input, options = {}) {
|
|
|
3453
3839
|
}
|
|
3454
3840
|
|
|
3455
3841
|
// src/signup-control-validate.ts
|
|
3456
|
-
var
|
|
3842
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3457
3843
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
3458
3844
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3459
3845
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
3460
|
-
var
|
|
3846
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set([
|
|
3461
3847
|
"schemaVersion",
|
|
3462
3848
|
"revision",
|
|
3463
3849
|
"generatedAt",
|
|
@@ -3473,8 +3859,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
3473
3859
|
]);
|
|
3474
3860
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
3475
3861
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3476
|
-
function
|
|
3477
|
-
for (const key of Object.keys(value)) if (!
|
|
3862
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
3863
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3478
3864
|
}
|
|
3479
3865
|
function text2(value, path, errors, max = 65536) {
|
|
3480
3866
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -3498,7 +3884,7 @@ function tiersOf(value, errors) {
|
|
|
3498
3884
|
errors.push(`${path} must be an object`);
|
|
3499
3885
|
return [];
|
|
3500
3886
|
}
|
|
3501
|
-
|
|
3887
|
+
unknownKeys2(item, TIER_KEYS2, path, errors);
|
|
3502
3888
|
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
3503
3889
|
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3504
3890
|
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
@@ -3534,7 +3920,7 @@ function applicationOf(value, errors) {
|
|
|
3534
3920
|
errors.push("document.application must be an object");
|
|
3535
3921
|
return void 0;
|
|
3536
3922
|
}
|
|
3537
|
-
|
|
3923
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
3538
3924
|
try {
|
|
3539
3925
|
const resolved = resolveApplication(input);
|
|
3540
3926
|
const copy = recordOf(input.copy);
|
|
@@ -3559,10 +3945,10 @@ function policyOf(value, errors) {
|
|
|
3559
3945
|
errors.push("document.policy must be an object");
|
|
3560
3946
|
return void 0;
|
|
3561
3947
|
}
|
|
3562
|
-
const
|
|
3563
|
-
|
|
3948
|
+
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
3949
|
+
unknownKeys2(input, allowed2, "document.policy", errors);
|
|
3564
3950
|
const result = {};
|
|
3565
|
-
for (const key of
|
|
3951
|
+
for (const key of allowed2) {
|
|
3566
3952
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3567
3953
|
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
3568
3954
|
}
|
|
@@ -3576,7 +3962,7 @@ function themeOf(value, errors) {
|
|
|
3576
3962
|
errors.push("document.theme.tokens must be an object");
|
|
3577
3963
|
return void 0;
|
|
3578
3964
|
}
|
|
3579
|
-
|
|
3965
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
3580
3966
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
3581
3967
|
let serializedLength = 0;
|
|
3582
3968
|
for (const [key, token] of Object.entries(tokens)) {
|
|
@@ -3596,7 +3982,7 @@ function validateSignupControlDocument(input) {
|
|
|
3596
3982
|
const errors = [];
|
|
3597
3983
|
const root = recordOf(input);
|
|
3598
3984
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3599
|
-
|
|
3985
|
+
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
3600
3986
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3601
3987
|
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
3602
3988
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
@@ -3605,8 +3991,8 @@ function validateSignupControlDocument(input) {
|
|
|
3605
3991
|
const appId = text2(source?.appId, "document.source.appId", errors, 63);
|
|
3606
3992
|
const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
3607
3993
|
const environment = text2(target?.environment, "document.target.environment", errors, 63);
|
|
3608
|
-
if (appId && !
|
|
3609
|
-
if (chapterId && !
|
|
3994
|
+
if (appId && !SLUG6.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3995
|
+
if (chapterId && !SLUG6.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3610
3996
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
3611
3997
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
3612
3998
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
@@ -3756,7 +4142,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
|
3756
4142
|
|
|
3757
4143
|
// src/membership-markdown.ts
|
|
3758
4144
|
var MAX_BENEFITS2 = 24;
|
|
3759
|
-
var
|
|
4145
|
+
var MAX_MARKDOWN3 = 65536;
|
|
3760
4146
|
var MAX_LABEL2 = 160;
|
|
3761
4147
|
var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
|
|
3762
4148
|
var MARKDOWN_HEADING = /^#{1,6}\s+/;
|
|
@@ -3790,8 +4176,8 @@ function markdownBenefits(lines, path, errors) {
|
|
|
3790
4176
|
}
|
|
3791
4177
|
function applyMembershipTierMarkdown(presentation, source) {
|
|
3792
4178
|
if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
|
|
3793
|
-
if (source.length >
|
|
3794
|
-
return { ok: false, errors: [`membership Markdown must be at most ${
|
|
4179
|
+
if (source.length > MAX_MARKDOWN3) {
|
|
4180
|
+
return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN3.toLocaleString("en-US")} characters`] };
|
|
3795
4181
|
}
|
|
3796
4182
|
if (!source.trim()) return { ok: true, value: { ...presentation } };
|
|
3797
4183
|
const errors = [];
|