@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.js
CHANGED
|
@@ -27,9 +27,9 @@ function canonicalValue(value) {
|
|
|
27
27
|
}
|
|
28
28
|
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
29
29
|
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
30
|
-
const
|
|
31
|
-
const entries = Object.keys(
|
|
32
|
-
const item =
|
|
30
|
+
const record3 = value;
|
|
31
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
32
|
+
const item = record3[key];
|
|
33
33
|
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
34
34
|
});
|
|
35
35
|
return `{${entries.join(",")}}`;
|
|
@@ -287,14 +287,14 @@ function hasDisclaimerAck(fields) {
|
|
|
287
287
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
288
288
|
function applicantProfile(chapter, fields) {
|
|
289
289
|
const app = chapter.application;
|
|
290
|
-
const
|
|
290
|
+
const allowed2 = (f) => app.profileFields.includes(f);
|
|
291
291
|
const profile = {};
|
|
292
292
|
for (const f of [...app.required, ...app.optional]) {
|
|
293
|
-
if (IDENTITY_FIELDS.has(f) || !
|
|
293
|
+
if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
|
|
294
294
|
const v = fields[f];
|
|
295
295
|
if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
|
|
296
296
|
}
|
|
297
|
-
if (fields.focus !== void 0 &&
|
|
297
|
+
if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
298
298
|
return Object.keys(profile).length > 0 ? profile : void 0;
|
|
299
299
|
}
|
|
300
300
|
async function submitApplication(db, chapter, fields, opts) {
|
|
@@ -649,6 +649,30 @@ var signupControls = {
|
|
|
649
649
|
receivedAt: attr4("number", { indexed: true })
|
|
650
650
|
}
|
|
651
651
|
};
|
|
652
|
+
var memberContentControls = {
|
|
653
|
+
attrs: {
|
|
654
|
+
id: id3(),
|
|
655
|
+
sourceId: attr4("string", { indexed: true }),
|
|
656
|
+
targetId: attr4("string", { indexed: true }),
|
|
657
|
+
environment: attr4("string", { indexed: true }),
|
|
658
|
+
revision: attr4("number", { indexed: true }),
|
|
659
|
+
digest: attr4("string", { indexed: true }),
|
|
660
|
+
document: attr4("json"),
|
|
661
|
+
active: attr4("boolean", { indexed: true }),
|
|
662
|
+
receivedAt: attr4("number", { indexed: true })
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
var memberContentLocal = {
|
|
666
|
+
attrs: {
|
|
667
|
+
id: id3(),
|
|
668
|
+
chapterId: attr4("string", { indexed: true }),
|
|
669
|
+
environment: attr4("string", { indexed: true }),
|
|
670
|
+
revision: attr4("number", { indexed: true }),
|
|
671
|
+
document: attr4("json"),
|
|
672
|
+
updatedAt: attr4("number", { indexed: true }),
|
|
673
|
+
updatedBy: attr4("string", { optional: true })
|
|
674
|
+
}
|
|
675
|
+
};
|
|
652
676
|
var groups = {
|
|
653
677
|
attrs: {
|
|
654
678
|
id: id3(),
|
|
@@ -720,7 +744,7 @@ var emailLog = {
|
|
|
720
744
|
sentAt: attr4("number", { indexed: true })
|
|
721
745
|
}
|
|
722
746
|
};
|
|
723
|
-
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
|
|
747
|
+
function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
|
|
724
748
|
const entities = {};
|
|
725
749
|
if (mode === "chapter") {
|
|
726
750
|
entities.applications = applicationsFor(application);
|
|
@@ -736,6 +760,10 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
736
760
|
entities.giftEntitlements = giftEntitlementsEntity;
|
|
737
761
|
entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
|
|
738
762
|
if (includeSignupControl) entities.signupControls = signupControls;
|
|
763
|
+
if (includeMemberContent) {
|
|
764
|
+
entities.memberContentControls = memberContentControls;
|
|
765
|
+
entities.memberContentLocal = memberContentLocal;
|
|
766
|
+
}
|
|
739
767
|
}
|
|
740
768
|
if (auth.source === "table") entities.admins = admins;
|
|
741
769
|
if (auth.superAdmins) entities.superAdmins = superAdmins;
|
|
@@ -1581,15 +1609,35 @@ function resolveSignupControl(input) {
|
|
|
1581
1609
|
return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
|
|
1582
1610
|
}
|
|
1583
1611
|
|
|
1584
|
-
// src/config.ts
|
|
1612
|
+
// src/config-member-content.ts
|
|
1585
1613
|
var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1614
|
+
var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
|
|
1615
|
+
function resolveMemberContent(input) {
|
|
1616
|
+
if (!input) return null;
|
|
1617
|
+
const parent = input.parent;
|
|
1618
|
+
if (parent && !SLUG3.test(parent.sourceId)) {
|
|
1619
|
+
throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
|
|
1620
|
+
}
|
|
1621
|
+
const secretName = parent?.secretName?.trim() || "member_content_control_secret";
|
|
1622
|
+
if (parent && !SECRET_NAME2.test(secretName)) {
|
|
1623
|
+
throw new Error("defineChapter.memberContent.parent.secretName: must be a vault-key identifier");
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
enabled: true,
|
|
1627
|
+
localAuthoring: input.localAuthoring !== false,
|
|
1628
|
+
parent: parent ? { sourceId: parent.sourceId, secretName } : null
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/config.ts
|
|
1633
|
+
var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
1586
1634
|
function isResolvedCrm(x) {
|
|
1587
1635
|
return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
|
|
1588
1636
|
}
|
|
1589
1637
|
function defineChapter(config) {
|
|
1590
1638
|
if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
|
|
1591
1639
|
const { id: id4, name } = config;
|
|
1592
|
-
if (typeof id4 !== "string" || !
|
|
1640
|
+
if (typeof id4 !== "string" || !SLUG4.test(id4)) {
|
|
1593
1641
|
throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
|
|
1594
1642
|
}
|
|
1595
1643
|
if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
|
|
@@ -1614,16 +1662,21 @@ function defineChapter(config) {
|
|
|
1614
1662
|
const copy = resolveChapterCopy(config.copy);
|
|
1615
1663
|
const network = resolveNetwork(config, crm);
|
|
1616
1664
|
const signupControl = resolveSignupControl(config.signupControl);
|
|
1665
|
+
const memberContent = resolveMemberContent(config.memberContent);
|
|
1617
1666
|
const formation = resolveLeaderFormation(config.formation, crm);
|
|
1618
1667
|
if (signupControl && mode !== "chapter") {
|
|
1619
1668
|
throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
|
|
1620
1669
|
}
|
|
1670
|
+
if (memberContent && mode !== "chapter") {
|
|
1671
|
+
throw new Error("defineChapter.memberContent: only chapter-mode sites can serve member content");
|
|
1672
|
+
}
|
|
1621
1673
|
const { schema, rules } = chapterDb(
|
|
1622
1674
|
mode,
|
|
1623
1675
|
auth,
|
|
1624
1676
|
network.targets.length > 0,
|
|
1625
1677
|
application,
|
|
1626
|
-
Boolean(signupControl)
|
|
1678
|
+
Boolean(signupControl),
|
|
1679
|
+
Boolean(memberContent)
|
|
1627
1680
|
);
|
|
1628
1681
|
const services = config.services ?? ["db", "calendar", "o11y"];
|
|
1629
1682
|
const account = config.account ?? "none";
|
|
@@ -1680,6 +1733,7 @@ function defineChapter(config) {
|
|
|
1680
1733
|
copy,
|
|
1681
1734
|
network,
|
|
1682
1735
|
signupControl,
|
|
1736
|
+
memberContent,
|
|
1683
1737
|
formation,
|
|
1684
1738
|
crm,
|
|
1685
1739
|
auth,
|
|
@@ -1974,12 +2028,332 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
1974
2028
|
}] : [],
|
|
1975
2029
|
// Formation metadata is public only when the host explicitly enables
|
|
1976
2030
|
// the bounded intake contract.
|
|
1977
|
-
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
|
|
2031
|
+
...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
|
|
2032
|
+
...chapter.memberContent ? [
|
|
2033
|
+
{ path: "/api/member-content", expectedStatus: 401 },
|
|
2034
|
+
{ path: "/api/admin/member-content", expectedStatus: 401 }
|
|
2035
|
+
] : []
|
|
1978
2036
|
],
|
|
1979
2037
|
runbooks
|
|
1980
2038
|
};
|
|
1981
2039
|
}
|
|
1982
2040
|
|
|
2041
|
+
// src/member-content-validation-helpers.ts
|
|
2042
|
+
var ID2 = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2043
|
+
var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2044
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2045
|
+
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
2046
|
+
var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
|
|
2047
|
+
var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
|
|
2048
|
+
var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
|
|
2049
|
+
var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
|
|
2050
|
+
var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
|
|
2051
|
+
var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
|
|
2052
|
+
var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
|
|
2053
|
+
var MAX_PAGES = 100;
|
|
2054
|
+
var MAX_MARKDOWN = 65536;
|
|
2055
|
+
var MAX_TEXT = 240;
|
|
2056
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2057
|
+
function unknownKeys(value, allowed2, path, errors) {
|
|
2058
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2059
|
+
}
|
|
2060
|
+
function textValue(value, path, errors, max = MAX_TEXT, required = false) {
|
|
2061
|
+
if (value === void 0) {
|
|
2062
|
+
if (required) errors.push(`${path} is required`);
|
|
2063
|
+
return void 0;
|
|
2064
|
+
}
|
|
2065
|
+
if (typeof value !== "string") {
|
|
2066
|
+
errors.push(`${path} must be a string`);
|
|
2067
|
+
return void 0;
|
|
2068
|
+
}
|
|
2069
|
+
const valueText = value.trim();
|
|
2070
|
+
if (required && !valueText) errors.push(`${path} is required`);
|
|
2071
|
+
if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
|
|
2072
|
+
return valueText;
|
|
2073
|
+
}
|
|
2074
|
+
function offerValue(value, path, errors, tiers2, audience) {
|
|
2075
|
+
const raw = record(value);
|
|
2076
|
+
if (!raw) {
|
|
2077
|
+
errors.push(`${path} must be an object`);
|
|
2078
|
+
return void 0;
|
|
2079
|
+
}
|
|
2080
|
+
unknownKeys(raw, OFFER_KEYS, path, errors);
|
|
2081
|
+
const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2082
|
+
if (tierId && !ID2.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2083
|
+
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2084
|
+
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2085
|
+
errors.push(`${path}.tierId must grant access to this page`);
|
|
2086
|
+
}
|
|
2087
|
+
const label = textValue(raw.label, `${path}.label`, errors);
|
|
2088
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
|
|
2089
|
+
const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
|
|
2090
|
+
return {
|
|
2091
|
+
tierId,
|
|
2092
|
+
...label ? { label } : {},
|
|
2093
|
+
...bodyMarkdown ? { bodyMarkdown } : {},
|
|
2094
|
+
...ctaLabel ? { ctaLabel } : {}
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// src/member-content-validation.ts
|
|
2099
|
+
function validateMemberContentDocument(input, options = {}) {
|
|
2100
|
+
const errors = [];
|
|
2101
|
+
const root = record(input);
|
|
2102
|
+
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
2103
|
+
unknownKeys(root, ROOT_KEYS, "document", errors);
|
|
2104
|
+
if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2105
|
+
if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
2106
|
+
if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
|
|
2107
|
+
const sourceRaw = record(root.source);
|
|
2108
|
+
let source = { type: "chapter", appId: "invalid" };
|
|
2109
|
+
if (!sourceRaw) errors.push("document.source must be an object");
|
|
2110
|
+
else {
|
|
2111
|
+
unknownKeys(sourceRaw, SOURCE_KEYS, "document.source", errors);
|
|
2112
|
+
const type = sourceRaw.type;
|
|
2113
|
+
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
2114
|
+
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
2115
|
+
if (appId && !ID2.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2116
|
+
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2117
|
+
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2118
|
+
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
2119
|
+
}
|
|
2120
|
+
let target;
|
|
2121
|
+
if (root.target !== void 0) {
|
|
2122
|
+
const targetRaw = record(root.target);
|
|
2123
|
+
if (!targetRaw) errors.push("document.target must be an object");
|
|
2124
|
+
else {
|
|
2125
|
+
unknownKeys(targetRaw, TARGET_KEYS, "document.target", errors);
|
|
2126
|
+
const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
|
|
2127
|
+
const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
|
|
2128
|
+
target = { chapterId, environment };
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
|
|
2132
|
+
if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
|
|
2133
|
+
if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
|
|
2134
|
+
const rawPages = Array.isArray(root.pages) ? root.pages : [];
|
|
2135
|
+
if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
|
|
2136
|
+
const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
|
|
2137
|
+
const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
|
|
2138
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2139
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2140
|
+
const pages = [];
|
|
2141
|
+
for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
|
|
2142
|
+
const path = `document.pages[${index}]`;
|
|
2143
|
+
const raw = record(value);
|
|
2144
|
+
if (!raw) {
|
|
2145
|
+
errors.push(`${path} must be an object`);
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
unknownKeys(raw, PAGE_KEYS, path, errors);
|
|
2149
|
+
const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2150
|
+
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2151
|
+
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2152
|
+
if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2153
|
+
if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2154
|
+
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2155
|
+
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
2156
|
+
ids.add(id4);
|
|
2157
|
+
slugs.add(slug);
|
|
2158
|
+
const navRaw = record(raw.navigation);
|
|
2159
|
+
let navigation = { section: "General", order: index };
|
|
2160
|
+
if (!navRaw) errors.push(`${path}.navigation must be an object`);
|
|
2161
|
+
else {
|
|
2162
|
+
unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
|
|
2163
|
+
const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
|
|
2164
|
+
const order = navRaw.order;
|
|
2165
|
+
if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
|
|
2166
|
+
errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
|
|
2167
|
+
}
|
|
2168
|
+
const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
|
|
2169
|
+
navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
|
|
2170
|
+
}
|
|
2171
|
+
const audienceRaw = record(raw.audience);
|
|
2172
|
+
let audience = { kind: "all-members" };
|
|
2173
|
+
if (!audienceRaw) errors.push(`${path}.audience must be an object`);
|
|
2174
|
+
else {
|
|
2175
|
+
unknownKeys(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
|
|
2176
|
+
if (audienceRaw.kind === "all-members") {
|
|
2177
|
+
if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
|
|
2178
|
+
} else if (audienceRaw.kind === "tiers") {
|
|
2179
|
+
if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
|
|
2180
|
+
errors.push(`${path}.audience.tierIds must be a non-empty array`);
|
|
2181
|
+
}
|
|
2182
|
+
const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
|
|
2183
|
+
const normalized = [];
|
|
2184
|
+
for (const [tierIndex, value2] of values.entries()) {
|
|
2185
|
+
const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
|
|
2186
|
+
if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
|
|
2187
|
+
if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
|
|
2188
|
+
normalized.push(tier);
|
|
2189
|
+
}
|
|
2190
|
+
audience = { kind: "tiers", tierIds: normalized };
|
|
2191
|
+
} else errors.push(`${path}.audience.kind must be all-members or tiers`);
|
|
2192
|
+
}
|
|
2193
|
+
const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
|
|
2194
|
+
const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
|
|
2195
|
+
const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
|
|
2196
|
+
let upsell;
|
|
2197
|
+
if (raw.upsell !== void 0) {
|
|
2198
|
+
const upsellRaw = record(raw.upsell);
|
|
2199
|
+
if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
|
|
2200
|
+
else {
|
|
2201
|
+
unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
|
|
2202
|
+
const owner = upsellRaw.owner;
|
|
2203
|
+
if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
|
|
2204
|
+
if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
|
|
2205
|
+
if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
|
|
2206
|
+
if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
|
|
2207
|
+
const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
|
|
2208
|
+
if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
|
|
2209
|
+
else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
pages.push({ id: id4, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
|
|
2213
|
+
}
|
|
2214
|
+
const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
|
|
2215
|
+
for (const [index, page] of pages.entries()) {
|
|
2216
|
+
const parentId = page.navigation.parentId;
|
|
2217
|
+
if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
|
|
2218
|
+
if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
|
|
2219
|
+
}
|
|
2220
|
+
for (const page of pages) {
|
|
2221
|
+
const seen = /* @__PURE__ */ new Set([page.id]);
|
|
2222
|
+
let parentId = page.navigation.parentId;
|
|
2223
|
+
while (parentId) {
|
|
2224
|
+
if (seen.has(parentId)) {
|
|
2225
|
+
errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
seen.add(parentId);
|
|
2229
|
+
parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
let upsellOverrides;
|
|
2233
|
+
if (root.upsellOverrides !== void 0) {
|
|
2234
|
+
if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
|
|
2235
|
+
if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
|
|
2236
|
+
const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
|
|
2237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2238
|
+
upsellOverrides = [];
|
|
2239
|
+
for (const [index, value] of values.entries()) {
|
|
2240
|
+
const path = `document.upsellOverrides[${index}]`;
|
|
2241
|
+
const raw = record(value);
|
|
2242
|
+
if (!raw) {
|
|
2243
|
+
errors.push(`${path} must be an object`);
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
unknownKeys(raw, OVERRIDE_KEYS, path, errors);
|
|
2247
|
+
const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
|
|
2248
|
+
if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
|
|
2249
|
+
seen.add(pageId);
|
|
2250
|
+
const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
|
|
2251
|
+
if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
|
|
2252
|
+
else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
|
|
2253
|
+
const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
|
|
2254
|
+
if (offer) upsellOverrides.push({ pageId, offer });
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
if (errors.length) return { ok: false, errors };
|
|
2258
|
+
return { ok: true, value: {
|
|
2259
|
+
schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
|
|
2260
|
+
revision: Number(root.revision),
|
|
2261
|
+
generatedAt: Number(root.generatedAt),
|
|
2262
|
+
source,
|
|
2263
|
+
...target ? { target } : {},
|
|
2264
|
+
pages,
|
|
2265
|
+
...upsellOverrides ? { upsellOverrides } : {}
|
|
2266
|
+
} };
|
|
2267
|
+
}
|
|
2268
|
+
function memberContentDocument(input, options = {}) {
|
|
2269
|
+
const result = validateMemberContentDocument(input, options);
|
|
2270
|
+
if (!result.ok) throw new TypeError(`Invalid member content document: ${result.errors.join("; ")}`);
|
|
2271
|
+
return result.value;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// src/member-content.ts
|
|
2275
|
+
var MEMBER_CONTENT_SCHEMA_VERSION = 1;
|
|
2276
|
+
var MEMBER_CONTENT_PATH = "/api/member-content";
|
|
2277
|
+
var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
|
|
2278
|
+
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);
|
|
2279
|
+
function composeMemberContent(parent, chapter) {
|
|
2280
|
+
const pages = [];
|
|
2281
|
+
const parentPages = /* @__PURE__ */ new Map();
|
|
2282
|
+
for (const page of parent?.pages ?? []) {
|
|
2283
|
+
const effective = { ...page, source: parent.source };
|
|
2284
|
+
parentPages.set(page.id, effective);
|
|
2285
|
+
pages.push(effective);
|
|
2286
|
+
}
|
|
2287
|
+
for (const override of chapter?.upsellOverrides ?? []) {
|
|
2288
|
+
const page = parentPages.get(override.pageId);
|
|
2289
|
+
if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
|
|
2290
|
+
}
|
|
2291
|
+
for (const page of chapter?.pages ?? []) {
|
|
2292
|
+
if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
|
|
2293
|
+
throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
|
|
2294
|
+
}
|
|
2295
|
+
pages.push({ ...page, source: chapter.source });
|
|
2296
|
+
}
|
|
2297
|
+
return {
|
|
2298
|
+
pages: pages.sort(pageOrder),
|
|
2299
|
+
parentRevision: parent?.revision ?? null,
|
|
2300
|
+
chapterRevision: chapter?.revision ?? null
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
|
|
2304
|
+
function projectMemberContent(content, viewer, tiers2) {
|
|
2305
|
+
const authority = new Map(tiers2.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
|
|
2306
|
+
return {
|
|
2307
|
+
viewer: { ...viewer },
|
|
2308
|
+
revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
|
|
2309
|
+
pages: content.pages.map((page) => {
|
|
2310
|
+
const canRead = allowed(page, viewer);
|
|
2311
|
+
const offer = !canRead ? page.upsell?.offer : void 0;
|
|
2312
|
+
const tier = offer ? authority.get(offer.tierId) : void 0;
|
|
2313
|
+
const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
|
|
2314
|
+
return {
|
|
2315
|
+
id: page.id,
|
|
2316
|
+
slug: page.slug,
|
|
2317
|
+
title: page.title,
|
|
2318
|
+
...page.summary ? { summary: page.summary } : {},
|
|
2319
|
+
navigation: page.navigation,
|
|
2320
|
+
audience: page.audience,
|
|
2321
|
+
source: page.source,
|
|
2322
|
+
access: canRead ? "allowed" : "locked",
|
|
2323
|
+
...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
|
|
2324
|
+
...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
|
|
2325
|
+
...upsell ? { upsell } : {}
|
|
2326
|
+
};
|
|
2327
|
+
})
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// src/member-content-canonical.ts
|
|
2332
|
+
var encoder2 = new TextEncoder();
|
|
2333
|
+
function canonicalValue2(value) {
|
|
2334
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2335
|
+
return JSON.stringify(value);
|
|
2336
|
+
}
|
|
2337
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2338
|
+
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2339
|
+
const record3 = value;
|
|
2340
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2341
|
+
const item = record3[key];
|
|
2342
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2343
|
+
});
|
|
2344
|
+
return `{${entries.join(",")}}`;
|
|
2345
|
+
}
|
|
2346
|
+
function canonicalMemberContent(value) {
|
|
2347
|
+
return canonicalValue2(value);
|
|
2348
|
+
}
|
|
2349
|
+
async function memberContentDigest(document) {
|
|
2350
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
|
|
2351
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2352
|
+
}
|
|
2353
|
+
async function createMemberContentEnvelope(document) {
|
|
2354
|
+
return { document, digest: await memberContentDigest(document) };
|
|
2355
|
+
}
|
|
2356
|
+
|
|
1983
2357
|
// src/email.ts
|
|
1984
2358
|
function render(template, vars) {
|
|
1985
2359
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -2174,10 +2548,10 @@ function resolveJoinContinuation(input) {
|
|
|
2174
2548
|
// src/gift-offers.ts
|
|
2175
2549
|
var GIFT_OFFER_CATALOG_VERSION = 1;
|
|
2176
2550
|
var MAX_GIFT_OFFERS = 20;
|
|
2177
|
-
var
|
|
2551
|
+
var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
|
|
2178
2552
|
var PRICE = /^price_[A-Za-z0-9]{12,}$/;
|
|
2179
2553
|
var CURRENCY = /^[a-z]{3}$/;
|
|
2180
|
-
var
|
|
2554
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2181
2555
|
var text = (value, path, errors, max) => {
|
|
2182
2556
|
if (typeof value !== "string" || !value.trim()) {
|
|
2183
2557
|
errors.push(`${path} is required`);
|
|
@@ -2188,7 +2562,7 @@ var text = (value, path, errors, max) => {
|
|
|
2188
2562
|
};
|
|
2189
2563
|
function validateGiftOfferCatalog(input, tierIds) {
|
|
2190
2564
|
const errors = [];
|
|
2191
|
-
const root =
|
|
2565
|
+
const root = record2(input);
|
|
2192
2566
|
if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
|
|
2193
2567
|
for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
|
|
2194
2568
|
if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
|
|
@@ -2199,18 +2573,18 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2199
2573
|
const offers = [];
|
|
2200
2574
|
for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
|
|
2201
2575
|
const path = `document.giftOffers.offers[${index}]`;
|
|
2202
|
-
const item =
|
|
2576
|
+
const item = record2(raw);
|
|
2203
2577
|
if (!item) {
|
|
2204
2578
|
errors.push(`${path} must be an object`);
|
|
2205
2579
|
continue;
|
|
2206
2580
|
}
|
|
2207
|
-
const
|
|
2208
|
-
for (const key of Object.keys(item)) if (!
|
|
2581
|
+
const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
|
|
2582
|
+
for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
2209
2583
|
const id4 = text(item.id, `${path}.id`, errors, 80);
|
|
2210
|
-
if (id4 && !
|
|
2584
|
+
if (id4 && !ID3.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2211
2585
|
if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
2212
2586
|
ids.add(id4);
|
|
2213
|
-
const targetRaw =
|
|
2587
|
+
const targetRaw = record2(item.target);
|
|
2214
2588
|
let target = { type: "entitlement", entitlementKey: "invalid" };
|
|
2215
2589
|
if (targetRaw?.type === "tier") {
|
|
2216
2590
|
const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
|
|
@@ -2218,7 +2592,7 @@ function validateGiftOfferCatalog(input, tierIds) {
|
|
|
2218
2592
|
target = { type: "tier", tierId };
|
|
2219
2593
|
} else if (targetRaw?.type === "entitlement") {
|
|
2220
2594
|
const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
|
|
2221
|
-
if (entitlementKey && !
|
|
2595
|
+
if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
|
|
2222
2596
|
target = { type: "entitlement", entitlementKey };
|
|
2223
2597
|
} else errors.push(`${path}.target.type must be tier or entitlement`);
|
|
2224
2598
|
if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
|
|
@@ -2303,34 +2677,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
|
|
|
2303
2677
|
if (readable && raw.length <= 64) return raw;
|
|
2304
2678
|
return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
|
|
2305
2679
|
}
|
|
2306
|
-
function normalizeSharedRecord(
|
|
2307
|
-
if ("input" in
|
|
2680
|
+
function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
|
|
2681
|
+
if ("input" in record3 && record3.version === 2) {
|
|
2308
2682
|
return {
|
|
2309
2683
|
version: 2,
|
|
2310
|
-
sourceId:
|
|
2311
|
-
sourceRecordId:
|
|
2312
|
-
type:
|
|
2313
|
-
input:
|
|
2684
|
+
sourceId: record3.source.siteId,
|
|
2685
|
+
sourceRecordId: record3.source.recordId,
|
|
2686
|
+
type: record3.type,
|
|
2687
|
+
input: record3.input
|
|
2314
2688
|
};
|
|
2315
2689
|
}
|
|
2316
|
-
if ("input" in
|
|
2690
|
+
if ("input" in record3) {
|
|
2317
2691
|
return {
|
|
2318
2692
|
version: 1,
|
|
2319
2693
|
sourceId: fallbackSourceId,
|
|
2320
|
-
sourceRecordId:
|
|
2321
|
-
type:
|
|
2322
|
-
input:
|
|
2694
|
+
sourceRecordId: record3.hubRecordId,
|
|
2695
|
+
type: record3.type,
|
|
2696
|
+
input: record3.input
|
|
2323
2697
|
};
|
|
2324
2698
|
}
|
|
2325
|
-
if ("type" in
|
|
2326
|
-
const input = { name:
|
|
2699
|
+
if ("type" in record3 && record3.type === "company") {
|
|
2700
|
+
const input = { name: record3.name };
|
|
2327
2701
|
for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
|
|
2328
|
-
if (
|
|
2702
|
+
if (record3[key]) input[key] = record3[key];
|
|
2329
2703
|
}
|
|
2330
2704
|
return {
|
|
2331
2705
|
version: 1,
|
|
2332
2706
|
sourceId: fallbackSourceId,
|
|
2333
|
-
sourceRecordId:
|
|
2707
|
+
sourceRecordId: record3.hubRecordId,
|
|
2334
2708
|
type: "company",
|
|
2335
2709
|
input
|
|
2336
2710
|
};
|
|
@@ -2338,28 +2712,28 @@ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
|
|
|
2338
2712
|
return {
|
|
2339
2713
|
version: 1,
|
|
2340
2714
|
sourceId: fallbackSourceId,
|
|
2341
|
-
sourceRecordId:
|
|
2715
|
+
sourceRecordId: record3.hubRecordId,
|
|
2342
2716
|
type: "person",
|
|
2343
|
-
input: sharedPersonInput(
|
|
2717
|
+
input: sharedPersonInput(record3)
|
|
2344
2718
|
};
|
|
2345
2719
|
}
|
|
2346
|
-
function sharedRecordFromCrm(crm,
|
|
2347
|
-
if (target.fields && !target.fields[
|
|
2348
|
-
throw new Error(`${target.name} does not accept "${
|
|
2720
|
+
function sharedRecordFromCrm(crm, record3, target, sourceId) {
|
|
2721
|
+
if (target.fields && !target.fields[record3.type]) {
|
|
2722
|
+
throw new Error(`${target.name} does not accept "${record3.type}" records`);
|
|
2349
2723
|
}
|
|
2350
|
-
const def = crm.type(
|
|
2351
|
-
const fields = target.fields?.[
|
|
2724
|
+
const def = crm.type(record3.type);
|
|
2725
|
+
const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
|
|
2352
2726
|
if (!fields) {
|
|
2353
|
-
throw new Error(`${target.name} requires an explicit field allowlist for "${
|
|
2727
|
+
throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
|
|
2354
2728
|
}
|
|
2355
2729
|
const nameField = def.nameField ?? "name";
|
|
2356
2730
|
const input = {};
|
|
2357
2731
|
for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
2358
|
-
const value =
|
|
2732
|
+
const value = record3.fields?.[field];
|
|
2359
2733
|
if (value !== void 0) input[field] = value;
|
|
2360
2734
|
}
|
|
2361
|
-
if (input[nameField] === void 0) input[nameField] =
|
|
2362
|
-
return sourceId ? { version: 2, source: { siteId: sourceId, recordId:
|
|
2735
|
+
if (input[nameField] === void 0) input[nameField] = record3.name;
|
|
2736
|
+
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
|
|
2363
2737
|
}
|
|
2364
2738
|
async function upsertPerson(deps, opts) {
|
|
2365
2739
|
const email = opts.email.toLowerCase();
|
|
@@ -2373,11 +2747,11 @@ async function upsertPerson(deps, opts) {
|
|
|
2373
2747
|
const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
|
|
2374
2748
|
return { recordId: created.id };
|
|
2375
2749
|
}
|
|
2376
|
-
async function findSharedRecord(deps,
|
|
2750
|
+
async function findSharedRecord(deps, record3, tag) {
|
|
2377
2751
|
const structured = await getRecordByOrigin(
|
|
2378
2752
|
{ crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
|
|
2379
|
-
|
|
2380
|
-
|
|
2753
|
+
record3.sourceId,
|
|
2754
|
+
record3.sourceRecordId
|
|
2381
2755
|
);
|
|
2382
2756
|
if (structured) return structured;
|
|
2383
2757
|
const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
|
|
@@ -2386,53 +2760,53 @@ async function findSharedRecord(deps, record2, tag) {
|
|
|
2386
2760
|
const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
|
|
2387
2761
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2388
2762
|
}
|
|
2389
|
-
const def = deps.crm.type(
|
|
2763
|
+
const def = deps.crm.type(record3.type);
|
|
2390
2764
|
const emailField = def.emailField;
|
|
2391
|
-
if (emailField && typeof
|
|
2392
|
-
const primaryEmail =
|
|
2765
|
+
if (emailField && typeof record3.input[emailField] === "string") {
|
|
2766
|
+
const primaryEmail = record3.input[emailField].toLowerCase();
|
|
2393
2767
|
const found = await deps.db.query({
|
|
2394
|
-
crm_record: { $: { where: { type:
|
|
2768
|
+
crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
|
|
2395
2769
|
});
|
|
2396
2770
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2397
2771
|
}
|
|
2398
|
-
const domain =
|
|
2772
|
+
const domain = record3.input.domain;
|
|
2399
2773
|
const domainSlot = def.fields.domain?.slot;
|
|
2400
2774
|
if (typeof domain === "string" && domainSlot) {
|
|
2401
2775
|
const found = await deps.db.query({
|
|
2402
|
-
crm_record: { $: { where: { type:
|
|
2776
|
+
crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
|
|
2403
2777
|
});
|
|
2404
2778
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2405
2779
|
}
|
|
2406
2780
|
const nameField = def.nameField ?? "name";
|
|
2407
|
-
const name =
|
|
2408
|
-
if (
|
|
2781
|
+
const name = record3.input[nameField];
|
|
2782
|
+
if (record3.type === "company" && typeof name === "string" && name.trim()) {
|
|
2409
2783
|
const found = await deps.db.query({
|
|
2410
|
-
crm_record: { $: { where: { type:
|
|
2784
|
+
crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
|
|
2411
2785
|
});
|
|
2412
2786
|
if (found.crm_record?.[0]) return found.crm_record[0];
|
|
2413
2787
|
}
|
|
2414
2788
|
return void 0;
|
|
2415
2789
|
}
|
|
2416
2790
|
async function projectSharedRecord(deps, shared, options = {}) {
|
|
2417
|
-
const
|
|
2418
|
-
if (!
|
|
2791
|
+
const record3 = normalizeSharedRecord(shared, options.sourceId);
|
|
2792
|
+
if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
|
|
2419
2793
|
throw new Error("type, source site id, and source record id must be non-empty");
|
|
2420
2794
|
}
|
|
2421
|
-
if (options.sourceId && options.sourceId !==
|
|
2795
|
+
if (options.sourceId && options.sourceId !== record3.sourceId) {
|
|
2422
2796
|
throw new Error("signed sender does not match payload source");
|
|
2423
2797
|
}
|
|
2424
|
-
const tag = networkSourceTag(
|
|
2798
|
+
const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
|
|
2425
2799
|
const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
|
|
2426
|
-
const existing = await findSharedRecord(deps,
|
|
2800
|
+
const existing = await findSharedRecord(deps, record3, tag);
|
|
2427
2801
|
let recordId;
|
|
2428
2802
|
if (existing && typeof existing.id === "string") {
|
|
2429
|
-
await updateRecord(crmDeps, { id: existing.id, input:
|
|
2803
|
+
await updateRecord(crmDeps, { id: existing.id, input: record3.input });
|
|
2430
2804
|
recordId = existing.id;
|
|
2431
2805
|
} else {
|
|
2432
|
-
recordId = `network_${shortHash(`${
|
|
2806
|
+
recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
|
|
2433
2807
|
await createRecord({ ...crmDeps, newId: () => recordId }, {
|
|
2434
|
-
type:
|
|
2435
|
-
input:
|
|
2808
|
+
type: record3.type,
|
|
2809
|
+
input: record3.input,
|
|
2436
2810
|
mutationId: `share-create:${tag}`
|
|
2437
2811
|
});
|
|
2438
2812
|
}
|
|
@@ -2442,10 +2816,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
|
|
|
2442
2816
|
);
|
|
2443
2817
|
await upsertRecordOrigin(crmDeps, {
|
|
2444
2818
|
recordId,
|
|
2445
|
-
sourceId:
|
|
2446
|
-
sourceRecordId:
|
|
2819
|
+
sourceId: record3.sourceId,
|
|
2820
|
+
sourceRecordId: record3.sourceRecordId,
|
|
2447
2821
|
...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
|
|
2448
|
-
payloadVersion:
|
|
2822
|
+
payloadVersion: record3.version
|
|
2449
2823
|
});
|
|
2450
2824
|
return { recordId };
|
|
2451
2825
|
}
|
|
@@ -2729,8 +3103,8 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
|
2729
3103
|
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
2730
3104
|
const page = Array.isArray(data) ? data : [];
|
|
2731
3105
|
for (const u of page) {
|
|
2732
|
-
const
|
|
2733
|
-
if (
|
|
3106
|
+
const record3 = toRecord(u);
|
|
3107
|
+
if (record3) out.push(record3);
|
|
2734
3108
|
}
|
|
2735
3109
|
if (page.length < PAGE) break;
|
|
2736
3110
|
}
|
|
@@ -2832,7 +3206,8 @@ function applicationSummary(app) {
|
|
|
2832
3206
|
meetingLink: app.meetingLink ?? null,
|
|
2833
3207
|
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
2834
3208
|
renewalAt: app.renewalAt ?? null,
|
|
2835
|
-
canceled: app.canceled === true
|
|
3209
|
+
canceled: app.canceled === true,
|
|
3210
|
+
tierId: app.tierId ?? null
|
|
2836
3211
|
};
|
|
2837
3212
|
}
|
|
2838
3213
|
function memberApplication(app, meeting, defaultTimezone) {
|
|
@@ -3136,7 +3511,7 @@ var SIGNUP_CONTROL_PATH = "/api/signup-control";
|
|
|
3136
3511
|
var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
|
|
3137
3512
|
var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3138
3513
|
var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
|
|
3139
|
-
var
|
|
3514
|
+
var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
|
|
3140
3515
|
var TIER_KEYS = /* @__PURE__ */ new Set([
|
|
3141
3516
|
"tierId",
|
|
3142
3517
|
"variant",
|
|
@@ -3155,12 +3530,12 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
|
|
|
3155
3530
|
var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
|
|
3156
3531
|
var MAX_TIERS = 20;
|
|
3157
3532
|
var MAX_BENEFITS = 24;
|
|
3158
|
-
var
|
|
3533
|
+
var MAX_MARKDOWN2 = 65536;
|
|
3159
3534
|
var MAX_LABEL = 160;
|
|
3160
3535
|
var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3161
|
-
function rejectUnknownKeys(value,
|
|
3536
|
+
function rejectUnknownKeys(value, allowed2, path, errors) {
|
|
3162
3537
|
for (const key of Object.keys(value)) {
|
|
3163
|
-
if (!
|
|
3538
|
+
if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3164
3539
|
}
|
|
3165
3540
|
}
|
|
3166
3541
|
function boundedString(value, path, errors, max, required = false) {
|
|
@@ -3181,7 +3556,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3181
3556
|
const errors = [];
|
|
3182
3557
|
const root = objectRecord(input);
|
|
3183
3558
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3184
|
-
rejectUnknownKeys(root,
|
|
3559
|
+
rejectUnknownKeys(root, ROOT_KEYS2, "document", errors);
|
|
3185
3560
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3186
3561
|
if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
|
|
3187
3562
|
errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
|
|
@@ -3235,7 +3610,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3235
3610
|
if (!rawCallout) errors.push(`${path}.callout must be an object`);
|
|
3236
3611
|
else {
|
|
3237
3612
|
rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
|
|
3238
|
-
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors,
|
|
3613
|
+
const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN2, true);
|
|
3239
3614
|
const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
|
|
3240
3615
|
if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
|
|
3241
3616
|
}
|
|
@@ -3257,10 +3632,10 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3257
3632
|
}
|
|
3258
3633
|
}
|
|
3259
3634
|
const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
|
|
3260
|
-
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors,
|
|
3635
|
+
const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN2);
|
|
3261
3636
|
const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
|
|
3262
3637
|
const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
|
|
3263
|
-
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors,
|
|
3638
|
+
const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN2);
|
|
3264
3639
|
const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
|
|
3265
3640
|
tiers2.push({
|
|
3266
3641
|
tierId,
|
|
@@ -3282,7 +3657,7 @@ function validateMembershipPageDocument(input, options = {}) {
|
|
|
3282
3657
|
if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
|
|
3283
3658
|
}
|
|
3284
3659
|
}
|
|
3285
|
-
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors,
|
|
3660
|
+
const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN2);
|
|
3286
3661
|
if (errors.length) return { ok: false, errors };
|
|
3287
3662
|
return {
|
|
3288
3663
|
ok: true,
|
|
@@ -3301,11 +3676,11 @@ function membershipPageDocument(input, options = {}) {
|
|
|
3301
3676
|
}
|
|
3302
3677
|
|
|
3303
3678
|
// src/signup-control-validate.ts
|
|
3304
|
-
var
|
|
3679
|
+
var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
|
|
3305
3680
|
var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
3306
3681
|
var TIER = /^[a-z][a-z0-9-]{0,79}$/;
|
|
3307
3682
|
var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
|
|
3308
|
-
var
|
|
3683
|
+
var ROOT_KEYS3 = /* @__PURE__ */ new Set([
|
|
3309
3684
|
"schemaVersion",
|
|
3310
3685
|
"revision",
|
|
3311
3686
|
"generatedAt",
|
|
@@ -3321,8 +3696,8 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
3321
3696
|
]);
|
|
3322
3697
|
var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
|
|
3323
3698
|
var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3324
|
-
function
|
|
3325
|
-
for (const key of Object.keys(value)) if (!
|
|
3699
|
+
function unknownKeys2(value, allowed2, path, errors) {
|
|
3700
|
+
for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
|
|
3326
3701
|
}
|
|
3327
3702
|
function text2(value, path, errors, max = 65536) {
|
|
3328
3703
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -3346,7 +3721,7 @@ function tiersOf(value, errors) {
|
|
|
3346
3721
|
errors.push(`${path} must be an object`);
|
|
3347
3722
|
return [];
|
|
3348
3723
|
}
|
|
3349
|
-
|
|
3724
|
+
unknownKeys2(item, TIER_KEYS2, path, errors);
|
|
3350
3725
|
const id4 = text2(item.id, `${path}.id`, errors, 80);
|
|
3351
3726
|
if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
3352
3727
|
if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
|
|
@@ -3382,7 +3757,7 @@ function applicationOf(value, errors) {
|
|
|
3382
3757
|
errors.push("document.application must be an object");
|
|
3383
3758
|
return void 0;
|
|
3384
3759
|
}
|
|
3385
|
-
|
|
3760
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
|
|
3386
3761
|
try {
|
|
3387
3762
|
const resolved = resolveApplication(input);
|
|
3388
3763
|
const copy = recordOf(input.copy);
|
|
@@ -3407,10 +3782,10 @@ function policyOf(value, errors) {
|
|
|
3407
3782
|
errors.push("document.policy must be an object");
|
|
3408
3783
|
return void 0;
|
|
3409
3784
|
}
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3785
|
+
const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
|
|
3786
|
+
unknownKeys2(input, allowed2, "document.policy", errors);
|
|
3412
3787
|
const result = {};
|
|
3413
|
-
for (const key of
|
|
3788
|
+
for (const key of allowed2) {
|
|
3414
3789
|
if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
|
|
3415
3790
|
result[key] = text2(input[key], `document.policy.${key}`, errors);
|
|
3416
3791
|
}
|
|
@@ -3424,7 +3799,7 @@ function themeOf(value, errors) {
|
|
|
3424
3799
|
errors.push("document.theme.tokens must be an object");
|
|
3425
3800
|
return void 0;
|
|
3426
3801
|
}
|
|
3427
|
-
|
|
3802
|
+
unknownKeys2(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
|
|
3428
3803
|
if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
|
|
3429
3804
|
let serializedLength = 0;
|
|
3430
3805
|
for (const [key, token] of Object.entries(tokens)) {
|
|
@@ -3444,7 +3819,7 @@ function validateSignupControlDocument(input) {
|
|
|
3444
3819
|
const errors = [];
|
|
3445
3820
|
const root = recordOf(input);
|
|
3446
3821
|
if (!root) return { ok: false, errors: ["document must be an object"] };
|
|
3447
|
-
|
|
3822
|
+
unknownKeys2(root, ROOT_KEYS3, "document", errors);
|
|
3448
3823
|
if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
|
|
3449
3824
|
if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
|
|
3450
3825
|
if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
|
|
@@ -3453,8 +3828,8 @@ function validateSignupControlDocument(input) {
|
|
|
3453
3828
|
const appId = text2(source?.appId, "document.source.appId", errors, 63);
|
|
3454
3829
|
const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
|
|
3455
3830
|
const environment = text2(target?.environment, "document.target.environment", errors, 63);
|
|
3456
|
-
if (appId && !
|
|
3457
|
-
if (chapterId && !
|
|
3831
|
+
if (appId && !SLUG6.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
3832
|
+
if (chapterId && !SLUG6.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
|
|
3458
3833
|
if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
|
|
3459
3834
|
if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
|
|
3460
3835
|
const tiers2 = tiersOf(root.tiers, errors);
|
|
@@ -3604,7 +3979,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
|
|
|
3604
3979
|
|
|
3605
3980
|
// src/membership-markdown.ts
|
|
3606
3981
|
var MAX_BENEFITS2 = 24;
|
|
3607
|
-
var
|
|
3982
|
+
var MAX_MARKDOWN3 = 65536;
|
|
3608
3983
|
var MAX_LABEL2 = 160;
|
|
3609
3984
|
var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
|
|
3610
3985
|
var MARKDOWN_HEADING = /^#{1,6}\s+/;
|
|
@@ -3638,8 +4013,8 @@ function markdownBenefits(lines, path, errors) {
|
|
|
3638
4013
|
}
|
|
3639
4014
|
function applyMembershipTierMarkdown(presentation, source) {
|
|
3640
4015
|
if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
|
|
3641
|
-
if (source.length >
|
|
3642
|
-
return { ok: false, errors: [`membership Markdown must be at most ${
|
|
4016
|
+
if (source.length > MAX_MARKDOWN3) {
|
|
4017
|
+
return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN3.toLocaleString("en-US")} characters`] };
|
|
3643
4018
|
}
|
|
3644
4019
|
if (!source.trim()) return { ok: true, value: { ...presentation } };
|
|
3645
4020
|
const errors = [];
|
|
@@ -3823,6 +4198,9 @@ export {
|
|
|
3823
4198
|
GIFT_OFFER_CATALOG_VERSION,
|
|
3824
4199
|
MAX_GIFT_OFFERS,
|
|
3825
4200
|
MEMBERSHIP_PAGE_RENDERER,
|
|
4201
|
+
MEMBER_CONTENT_CONTROL_PATH,
|
|
4202
|
+
MEMBER_CONTENT_PATH,
|
|
4203
|
+
MEMBER_CONTENT_SCHEMA_VERSION,
|
|
3826
4204
|
SCHEDULING_DEFAULTS,
|
|
3827
4205
|
SIGNUP_CONTROL_PATH,
|
|
3828
4206
|
SIGNUP_CONTROL_SCHEMA_VERSION,
|
|
@@ -3843,6 +4221,7 @@ export {
|
|
|
3843
4221
|
canChangeRole,
|
|
3844
4222
|
canTransition,
|
|
3845
4223
|
canceledPatch,
|
|
4224
|
+
canonicalMemberContent,
|
|
3846
4225
|
canonicalSignupControl,
|
|
3847
4226
|
chapterBrandFromTokens,
|
|
3848
4227
|
chapterDb,
|
|
@@ -3854,10 +4233,12 @@ export {
|
|
|
3854
4233
|
clerkListUsers,
|
|
3855
4234
|
clerkSetRole,
|
|
3856
4235
|
clerkUserRequest,
|
|
4236
|
+
composeMemberContent,
|
|
3857
4237
|
createAdmissionGrantEnvelope,
|
|
3858
4238
|
createChapterIntegration,
|
|
3859
4239
|
createClerkInvitation,
|
|
3860
4240
|
createClerkUser,
|
|
4241
|
+
createMemberContentEnvelope,
|
|
3861
4242
|
createSignupControlEnvelope,
|
|
3862
4243
|
dashboardMetricData,
|
|
3863
4244
|
defaultCrm,
|
|
@@ -3893,6 +4274,8 @@ export {
|
|
|
3893
4274
|
meetingCreateRow,
|
|
3894
4275
|
meetingRescheduleUpdate,
|
|
3895
4276
|
memberApplication,
|
|
4277
|
+
memberContentDigest,
|
|
4278
|
+
memberContentDocument,
|
|
3896
4279
|
memberSession,
|
|
3897
4280
|
membershipPageDocument,
|
|
3898
4281
|
membershipTierPresentationFromMarkdown,
|
|
@@ -3905,6 +4288,7 @@ export {
|
|
|
3905
4288
|
personInputFromApp,
|
|
3906
4289
|
planDelivery,
|
|
3907
4290
|
projectApplicant,
|
|
4291
|
+
projectMemberContent,
|
|
3908
4292
|
projectSharedRecord,
|
|
3909
4293
|
publicGiftOffers,
|
|
3910
4294
|
reconcileMeetings,
|
|
@@ -3919,6 +4303,7 @@ export {
|
|
|
3919
4303
|
resolveChapterCopy,
|
|
3920
4304
|
resolveJoinContinuation,
|
|
3921
4305
|
resolveLeaderFormation,
|
|
4306
|
+
resolveMemberContent,
|
|
3922
4307
|
resolvePipeline,
|
|
3923
4308
|
resolveScheduling,
|
|
3924
4309
|
resolveSignupControl,
|
|
@@ -3942,6 +4327,7 @@ export {
|
|
|
3942
4327
|
updateClerkUserMetadataByEmail,
|
|
3943
4328
|
validateAdmissionGrantDocument,
|
|
3944
4329
|
validateGiftOfferCatalog,
|
|
4330
|
+
validateMemberContentDocument,
|
|
3945
4331
|
validateMembershipPageDocument,
|
|
3946
4332
|
validateScheduling,
|
|
3947
4333
|
validateSignupControlDocument,
|