@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/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 record2 = value;
31
- const entries = Object.keys(record2).sort().flatMap((key) => {
32
- const item = record2[key];
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 allowed = (f) => app.profileFields.includes(f);
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) || !allowed(f)) continue;
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 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
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" || !SLUG3.test(id4)) {
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 ID2 = /^[a-z][a-z0-9-]{1,79}$/;
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 record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
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 = record(input);
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 = record(raw);
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 allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
2208
- for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
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 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
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 = record(item.target);
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 && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
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(record2, fallbackSourceId = "legacy-source") {
2307
- if ("input" in record2 && record2.version === 2) {
2680
+ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
2681
+ if ("input" in record3 && record3.version === 2) {
2308
2682
  return {
2309
2683
  version: 2,
2310
- sourceId: record2.source.siteId,
2311
- sourceRecordId: record2.source.recordId,
2312
- type: record2.type,
2313
- input: record2.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 record2) {
2690
+ if ("input" in record3) {
2317
2691
  return {
2318
2692
  version: 1,
2319
2693
  sourceId: fallbackSourceId,
2320
- sourceRecordId: record2.hubRecordId,
2321
- type: record2.type,
2322
- input: record2.input
2694
+ sourceRecordId: record3.hubRecordId,
2695
+ type: record3.type,
2696
+ input: record3.input
2323
2697
  };
2324
2698
  }
2325
- if ("type" in record2 && record2.type === "company") {
2326
- const input = { name: record2.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 (record2[key]) input[key] = record2[key];
2702
+ if (record3[key]) input[key] = record3[key];
2329
2703
  }
2330
2704
  return {
2331
2705
  version: 1,
2332
2706
  sourceId: fallbackSourceId,
2333
- sourceRecordId: record2.hubRecordId,
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: record2.hubRecordId,
2715
+ sourceRecordId: record3.hubRecordId,
2342
2716
  type: "person",
2343
- input: sharedPersonInput(record2)
2717
+ input: sharedPersonInput(record3)
2344
2718
  };
2345
2719
  }
2346
- function sharedRecordFromCrm(crm, record2, target, sourceId) {
2347
- if (target.fields && !target.fields[record2.type]) {
2348
- throw new Error(`${target.name} does not accept "${record2.type}" records`);
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(record2.type);
2351
- const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
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 "${record2.type}" records`);
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 = record2.fields?.[field];
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] = record2.name;
2362
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
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, record2, tag) {
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
- record2.sourceId,
2380
- record2.sourceRecordId
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(record2.type);
2763
+ const def = deps.crm.type(record3.type);
2390
2764
  const emailField = def.emailField;
2391
- if (emailField && typeof record2.input[emailField] === "string") {
2392
- const primaryEmail = record2.input[emailField].toLowerCase();
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: record2.type, primaryEmail }, limit: 1 } }
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 = record2.input.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: record2.type, [domainSlot]: domain }, limit: 1 } }
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 = record2.input[nameField];
2408
- if (record2.type === "company" && typeof name === "string" && name.trim()) {
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: record2.type, name: name.trim() }, limit: 1 } }
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 record2 = normalizeSharedRecord(shared, options.sourceId);
2418
- if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
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 !== record2.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(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
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, record2, tag);
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: record2.input });
2803
+ await updateRecord(crmDeps, { id: existing.id, input: record3.input });
2430
2804
  recordId = existing.id;
2431
2805
  } else {
2432
- recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
2806
+ recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
2433
2807
  await createRecord({ ...crmDeps, newId: () => recordId }, {
2434
- type: record2.type,
2435
- input: record2.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: record2.sourceId,
2446
- sourceRecordId: record2.sourceRecordId,
2819
+ sourceId: record3.sourceId,
2820
+ sourceRecordId: record3.sourceRecordId,
2447
2821
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
2448
- payloadVersion: record2.version
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 record2 = toRecord(u);
2733
- if (record2) out.push(record2);
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 ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
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 MAX_MARKDOWN = 65536;
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, allowed, path, errors) {
3536
+ function rejectUnknownKeys(value, allowed2, path, errors) {
3162
3537
  for (const key of Object.keys(value)) {
3163
- if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
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, ROOT_KEYS, "document", errors);
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, MAX_MARKDOWN, true);
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, MAX_MARKDOWN);
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, MAX_MARKDOWN);
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, MAX_MARKDOWN);
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 SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
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 ROOT_KEYS2 = /* @__PURE__ */ new Set([
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 unknownKeys(value, allowed, path, errors) {
3325
- for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
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
- unknownKeys(item, TIER_KEYS2, path, errors);
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
- unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
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 allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
3411
- unknownKeys(input, allowed, "document.policy", errors);
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 allowed) {
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
- unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
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
- unknownKeys(root, ROOT_KEYS2, "document", errors);
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 && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
3457
- if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
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 MAX_MARKDOWN2 = 65536;
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 > MAX_MARKDOWN2) {
3642
- return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN2.toLocaleString("en-US")} characters`] };
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,