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