@odla-ai/chapter 0.35.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
@@ -20,10 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ADMISSION_GRANTS_PATH: () => ADMISSION_GRANTS_PATH,
24
+ ADMISSION_GRANTS_SCHEMA_VERSION: () => ADMISSION_GRANTS_SCHEMA_VERSION,
23
25
  CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
24
26
  DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
25
27
  DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
28
+ GIFT_OFFER_CATALOG_VERSION: () => GIFT_OFFER_CATALOG_VERSION,
29
+ MAX_GIFT_OFFERS: () => MAX_GIFT_OFFERS,
26
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,
27
34
  SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
28
35
  SIGNUP_CONTROL_PATH: () => SIGNUP_CONTROL_PATH,
29
36
  SIGNUP_CONTROL_SCHEMA_VERSION: () => SIGNUP_CONTROL_SCHEMA_VERSION,
@@ -44,6 +51,7 @@ __export(index_exports, {
44
51
  canChangeRole: () => canChangeRole,
45
52
  canTransition: () => canTransition,
46
53
  canceledPatch: () => canceledPatch,
54
+ canonicalMemberContent: () => canonicalMemberContent,
47
55
  canonicalSignupControl: () => canonicalSignupControl,
48
56
  chapterBrandFromTokens: () => chapterBrandFromTokens,
49
57
  chapterDb: () => chapterDb,
@@ -55,9 +63,12 @@ __export(index_exports, {
55
63
  clerkListUsers: () => clerkListUsers,
56
64
  clerkSetRole: () => clerkSetRole,
57
65
  clerkUserRequest: () => clerkUserRequest,
66
+ composeMemberContent: () => composeMemberContent,
67
+ createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
58
68
  createChapterIntegration: () => createChapterIntegration,
59
69
  createClerkInvitation: () => createClerkInvitation,
60
70
  createClerkUser: () => createClerkUser,
71
+ createMemberContentEnvelope: () => createMemberContentEnvelope,
61
72
  createSignupControlEnvelope: () => createSignupControlEnvelope,
62
73
  dashboardMetricData: () => dashboardMetricData,
63
74
  defaultCrm: () => defaultCrm,
@@ -65,12 +76,15 @@ __export(index_exports, {
65
76
  emailGroupFrom: () => emailGroupFrom,
66
77
  endForSlot: () => endForSlot,
67
78
  exportChapterPromotion: () => exportChapterPromotion,
79
+ findAdmissionGrant: () => findAdmissionGrant,
68
80
  findApplicationRef: () => findApplicationRef,
81
+ findGiftOffer: () => findGiftOffer,
69
82
  findTier: () => findTier,
70
83
  firstPaymentPatch: () => firstPaymentPatch,
71
84
  formatChapterCopy: () => formatChapterCopy,
72
85
  formationFields: () => formationFields,
73
86
  getVaultSecret: () => getVaultSecret,
87
+ giftOfferActive: () => giftOfferActive,
74
88
  hasDisclaimerAck: () => hasDisclaimerAck,
75
89
  hydrateSignupControlTheme: () => hydrateSignupControlTheme,
76
90
  injectSignupControlThemeHead: () => injectSignupControlThemeHead,
@@ -83,15 +97,20 @@ __export(index_exports, {
83
97
  joinConfig: () => joinConfig,
84
98
  joinConfigTiers: () => joinConfigTiers,
85
99
  leaderCrmConfig: () => leaderCrmConfig,
100
+ loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
86
101
  loadOfferableTiers: () => loadOfferableTiers,
87
102
  loadTiers: () => loadTiers,
103
+ materializeAdmissionGrants: () => materializeAdmissionGrants,
88
104
  meetingCreateRow: () => meetingCreateRow,
89
105
  meetingRescheduleUpdate: () => meetingRescheduleUpdate,
90
106
  memberApplication: () => memberApplication,
107
+ memberContentDigest: () => memberContentDigest,
108
+ memberContentDocument: () => memberContentDocument,
91
109
  memberSession: () => memberSession,
92
110
  membershipPageDocument: () => membershipPageDocument,
93
111
  membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
94
112
  networkSourceTag: () => networkSourceTag,
113
+ normalizeAdmissionEmail: () => normalizeAdmissionEmail,
95
114
  normalizeSharedRecord: () => normalizeSharedRecord,
96
115
  normalizeWebhookEvent: () => normalizeWebhookEvent,
97
116
  offerableTiers: () => offerableTiers,
@@ -99,7 +118,9 @@ __export(index_exports, {
99
118
  personInputFromApp: () => personInputFromApp,
100
119
  planDelivery: () => planDelivery,
101
120
  projectApplicant: () => projectApplicant,
121
+ projectMemberContent: () => projectMemberContent,
102
122
  projectSharedRecord: () => projectSharedRecord,
123
+ publicGiftOffers: () => publicGiftOffers,
103
124
  reconcileMeetings: () => reconcileMeetings,
104
125
  refundedPatch: () => refundedPatch,
105
126
  render: () => render,
@@ -110,7 +131,9 @@ __export(index_exports, {
110
131
  resolveApplication: () => resolveApplication,
111
132
  resolveAuth: () => resolveAuth,
112
133
  resolveChapterCopy: () => resolveChapterCopy,
134
+ resolveJoinContinuation: () => resolveJoinContinuation,
113
135
  resolveLeaderFormation: () => resolveLeaderFormation,
136
+ resolveMemberContent: () => resolveMemberContent,
114
137
  resolvePipeline: () => resolvePipeline,
115
138
  resolveScheduling: () => resolveScheduling,
116
139
  resolveSignupControl: () => resolveSignupControl,
@@ -132,6 +155,9 @@ __export(index_exports, {
132
155
  tierPayable: () => tierPayable,
133
156
  updateClerkUserMetadata: () => updateClerkUserMetadata,
134
157
  updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
158
+ validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
159
+ validateGiftOfferCatalog: () => validateGiftOfferCatalog,
160
+ validateMemberContentDocument: () => validateMemberContentDocument,
135
161
  validateMembershipPageDocument: () => validateMembershipPageDocument,
136
162
  validateScheduling: () => validateScheduling,
137
163
  validateSignupControlDocument: () => validateSignupControlDocument,
@@ -157,6 +183,213 @@ async function applicationIdForSubmission(submissionId) {
157
183
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
158
184
  }
159
185
 
186
+ // src/admission-grants-contract.ts
187
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
188
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
189
+
190
+ // src/signup-control-canonical.ts
191
+ var encoder = new TextEncoder();
192
+ function canonicalValue(value) {
193
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
194
+ return JSON.stringify(value);
195
+ }
196
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
197
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
198
+ const record3 = value;
199
+ const entries = Object.keys(record3).sort().flatMap((key) => {
200
+ const item = record3[key];
201
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
202
+ });
203
+ return `{${entries.join(",")}}`;
204
+ }
205
+ function canonicalSignupControl(value) {
206
+ return canonicalValue(value);
207
+ }
208
+ async function signupControlDigest(document) {
209
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
210
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
211
+ }
212
+ async function createSignupControlEnvelope(document) {
213
+ return { document, digest: await signupControlDigest(document) };
214
+ }
215
+
216
+ // src/admission-grants.ts
217
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
218
+ var MAX_GRANTS = 2e3;
219
+ var MAX_NOTE = 2e3;
220
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
221
+ var isAdmissionEmail = (value) => EMAIL_RE.test(value);
222
+ function normalizeAdmissionEmail(email) {
223
+ return email.trim().toLowerCase();
224
+ }
225
+ function validateAdmissionGrantDocument(input) {
226
+ const errors = [];
227
+ const root = input && typeof input === "object" ? input : {};
228
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
229
+ const revision = Number(root.revision);
230
+ const generatedAt = Number(root.generatedAt);
231
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
232
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
233
+ const source = root.source && typeof root.source === "object" ? root.source : {};
234
+ const target = root.target && typeof root.target === "object" ? root.target : {};
235
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
236
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
237
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
238
+ const environment = typeof target.environment === "string" ? target.environment : "";
239
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
240
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
241
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
242
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
243
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
244
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
245
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
246
+ const grants = [];
247
+ const seenIds = /* @__PURE__ */ new Set();
248
+ const seenEmails = /* @__PURE__ */ new Set();
249
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
250
+ const grant = raw && typeof raw === "object" ? raw : {};
251
+ const id4 = typeof grant.id === "string" ? grant.id : "";
252
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
253
+ const normalized = normalizeAdmissionEmail(email);
254
+ const decision = grant.decision;
255
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
256
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
257
+ if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
258
+ if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
259
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
260
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
261
+ if (decision !== "waive_interview" && decision !== "require_interview") {
262
+ errors.push(`document.grants[${index}].decision is invalid`);
263
+ }
264
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
265
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
266
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
267
+ }
268
+ seenIds.add(id4);
269
+ seenEmails.add(normalized);
270
+ grants.push({
271
+ id: id4,
272
+ email,
273
+ decision,
274
+ ...note ? { note } : {},
275
+ ...expiresAt !== void 0 ? { expiresAt } : {}
276
+ });
277
+ }
278
+ if (errors.length) return { ok: false, errors };
279
+ return {
280
+ ok: true,
281
+ value: {
282
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
283
+ revision,
284
+ generatedAt,
285
+ source: { appId: sourceId, actor: sourceActor },
286
+ target: { chapterId, environment },
287
+ grants
288
+ }
289
+ };
290
+ }
291
+ async function createAdmissionGrantEnvelope(document) {
292
+ const checked = validateAdmissionGrantDocument(document);
293
+ if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
294
+ return { document: checked.value, digest: await signupControlDigest(checked.value) };
295
+ }
296
+ var rows = (value) => Array.isArray(value) ? value : [];
297
+ function asGrant(row) {
298
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
299
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
300
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
301
+ return row;
302
+ }
303
+ async function findAdmissionGrant(db, targetId, email, now) {
304
+ const emailNormalized = normalizeAdmissionEmail(email);
305
+ if (!isAdmissionEmail(emailNormalized)) return null;
306
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
307
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
308
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
309
+ )).sort((a, b) => {
310
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
311
+ return scope || b.createdAt - a.createdAt;
312
+ });
313
+ return active[0] ?? null;
314
+ }
315
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
316
+ const where = { sourceId, targetId, environment };
317
+ if (db.aggregate) {
318
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
319
+ }
320
+ const found = await db.query({ admissionGrantRevisions: {} });
321
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
322
+ }
323
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
324
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
325
+ const result = await db.query({ admissionGrantRevisions: { $: {
326
+ where: { sourceId, targetId, environment },
327
+ order: { revision: "desc" },
328
+ limit: 1
329
+ } } });
330
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
331
+ return row ? row : null;
332
+ }
333
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
334
+ const { document, digest } = envelope;
335
+ const current = rows((await db.query({ admissionGrants: { $: {
336
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
337
+ limit: MAX_GRANTS
338
+ } } })).admissionGrants);
339
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
340
+ const ops = [];
341
+ for (const row of current) {
342
+ const id4 = String(row.id ?? "");
343
+ if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
344
+ active: false,
345
+ revokedAt: receivedAt,
346
+ revokedBy: document.source.actor,
347
+ updatedAt: receivedAt,
348
+ updatedBy: document.source.actor
349
+ } });
350
+ }
351
+ for (const grant of document.grants) {
352
+ const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
353
+ const existing = current.find((row) => row.id === id4);
354
+ ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
355
+ id: id4,
356
+ sourceGrantId: grant.id,
357
+ sourceScope: "parent",
358
+ sourceId: document.source.appId,
359
+ targetId: document.target.chapterId,
360
+ email: grant.email,
361
+ emailNormalized: normalizeAdmissionEmail(grant.email),
362
+ decision: grant.decision,
363
+ ...grant.note ? { note: grant.note } : {},
364
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
365
+ active: true,
366
+ sourceRevision: document.revision,
367
+ createdAt: Number(existing?.createdAt ?? receivedAt),
368
+ createdBy: String(existing?.createdBy ?? document.source.actor),
369
+ updatedAt: receivedAt,
370
+ updatedBy: document.source.actor
371
+ } });
372
+ const retract = [
373
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
374
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
375
+ ];
376
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
377
+ }
378
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
379
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
380
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
381
+ id: revisionId,
382
+ sourceId: document.source.appId,
383
+ targetId: document.target.chapterId,
384
+ environment: document.target.environment,
385
+ revision: document.revision,
386
+ digest,
387
+ active: true,
388
+ receivedAt
389
+ } });
390
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
391
+ }
392
+
160
393
  // src/member.ts
161
394
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
162
395
  var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
@@ -208,9 +441,9 @@ function resolveApplication(a) {
208
441
  validateEmail: a?.validateEmail ?? true
209
442
  };
210
443
  }
211
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
444
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
212
445
  function isValidEmail(value) {
213
- return typeof value === "string" && EMAIL_RE.test(value);
446
+ return typeof value === "string" && EMAIL_RE2.test(value);
214
447
  }
215
448
  function clampArray(value, max) {
216
449
  if (!Array.isArray(value)) return value;
@@ -222,14 +455,14 @@ function hasDisclaimerAck(fields) {
222
455
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
223
456
  function applicantProfile(chapter, fields) {
224
457
  const app = chapter.application;
225
- const allowed = (f) => app.profileFields.includes(f);
458
+ const allowed2 = (f) => app.profileFields.includes(f);
226
459
  const profile = {};
227
460
  for (const f of [...app.required, ...app.optional]) {
228
- if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
461
+ if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
229
462
  const v = fields[f];
230
463
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
231
464
  }
232
- 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);
233
466
  return Object.keys(profile).length > 0 ? profile : void 0;
234
467
  }
235
468
  async function submitApplication(db, chapter, fields, opts) {
@@ -252,14 +485,23 @@ async function submitApplication(db, chapter, fields, opts) {
252
485
  if (app.requireDisclaimerAck && !acked) {
253
486
  return { ok: false, error: "disclaimerAck is required" };
254
487
  }
255
- const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
256
- const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
488
+ const id4 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
489
+ const row = { id: id4, status: chapter.pipeline.initial, createdAt: opts.now };
257
490
  for (const f of [...app.required, ...app.optional]) {
258
491
  if (fieldStates[f]?.visible === false) continue;
259
492
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
260
493
  }
261
494
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
262
495
  if (opts.groupId) row.groupId = opts.groupId;
496
+ if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
497
+ row.emailNormalized = normalizeAdmissionEmail(fields.email);
498
+ }
499
+ if (opts.admissionGrant?.decision === "waive_interview") {
500
+ row.admissionGrantId = opts.admissionGrant.id;
501
+ row.admissionSource = opts.admissionGrant.sourceScope;
502
+ row.interviewWaivedAt = opts.now;
503
+ }
504
+ if (opts.giftOfferId) row.giftOfferId = opts.giftOfferId;
263
505
  if (typeof fields.tierId === "string" && fields.tierId) {
264
506
  if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
265
507
  return { ok: false, error: "tierId is not an offered tier" };
@@ -267,11 +509,20 @@ async function submitApplication(db, chapter, fields, opts) {
267
509
  row.tierId = fields.tierId;
268
510
  }
269
511
  if (acked) row.disclaimerAckAt = opts.now;
512
+ const ops = [
513
+ { t: "update", ns: "applications", id: id4, attrs: row }
514
+ ];
515
+ if (opts.admissionGrant?.decision === "waive_interview") {
516
+ ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
517
+ consumedAt: opts.now,
518
+ consumedApplicationId: id4
519
+ } });
520
+ }
270
521
  const { duplicate } = await db.transact(
271
- [{ t: "update", ns: "applications", id: id2, attrs: row }],
522
+ ops,
272
523
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
273
524
  );
274
- return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
525
+ return { ok: true, id: id4, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
275
526
  }
276
527
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
277
528
  return {
@@ -291,93 +542,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
291
542
  };
292
543
  }
293
544
 
294
- // src/schema.ts
295
- function attr(type, flags = {}) {
296
- return {
297
- type,
298
- unique: flags.unique ?? false,
299
- indexed: flags.indexed ?? false,
300
- optional: flags.optional ?? false
301
- };
302
- }
303
- var id = () => attr("string", { unique: true, indexed: true });
304
- var admins = {
305
- attrs: {
306
- id: id(),
307
- email: attr("string", { unique: true, indexed: true }),
308
- name: attr("string", { optional: true }),
309
- note: attr("string", { optional: true })
310
- }
311
- };
312
- var superAdmins = {
313
- attrs: {
314
- id: id(),
315
- email: attr("string", { unique: true, indexed: true }),
316
- note: attr("string", { optional: true }),
317
- createdAt: attr("number", { indexed: true })
318
- }
319
- };
320
- var networkNotes = {
321
- attrs: {
322
- id: id(),
323
- recordKey: attr("string", { indexed: true }),
324
- targetId: attr("string", { indexed: true }),
325
- recordType: attr("string", { indexed: true }),
326
- recordId: attr("string", { indexed: true }),
327
- body: attr("string"),
328
- authorId: attr("string", { indexed: true }),
329
- authorEmail: attr("string", { optional: true }),
330
- createdAt: attr("number", { indexed: true })
331
- }
332
- };
333
- var signupControls = {
334
- attrs: {
335
- id: id(),
336
- sourceId: attr("string", { indexed: true }),
337
- targetId: attr("string", { indexed: true }),
338
- environment: attr("string", { indexed: true }),
339
- revision: attr("number", { indexed: true }),
340
- digest: attr("string", { indexed: true }),
341
- document: attr("json"),
342
- active: attr("boolean", { indexed: true }),
343
- receivedAt: attr("number", { indexed: true })
344
- }
345
- };
346
- var applications = {
347
- attrs: {
348
- id: id(),
349
- firstName: attr("string"),
350
- lastName: attr("string"),
351
- email: attr("string", { indexed: true }),
352
- referral: attr("string"),
353
- referralName: attr("string", { optional: true }),
354
- whoYouAre: attr("string"),
355
- focus: attr("json"),
356
- linkedin: attr("string", { optional: true }),
357
- message: attr("string"),
358
- status: attr("string", { indexed: true }),
359
- createdAt: attr("number", { indexed: true }),
360
- meetingAt: attr("number", { indexed: true, optional: true }),
361
- meetingLink: attr("string", { optional: true }),
362
- clerkUserId: attr("string", { indexed: true, optional: true }),
363
- // One-time/effect-repair marker. Absent means /api/me should copy the
364
- // allowlisted application snapshot into Clerk private_metadata and remove
365
- // the former public_metadata copy.
366
- clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
367
- phone: attr("string", { optional: true }),
368
- state: attr("string", { optional: true }),
369
- groupId: attr("string", { indexed: true, optional: true }),
370
- // The tier this application joined on. Absent on rows that predate tiers.
371
- tierId: attr("string", { indexed: true, optional: true }),
372
- stripeCustomerId: attr("string", { indexed: true, optional: true }),
373
- stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
374
- renewalAt: attr("number", { optional: true }),
375
- disclaimerAckAt: attr("number", { optional: true }),
376
- refundPolicyAckAt: attr("number", { optional: true }),
377
- prepEmailSentAt: attr("number", { optional: true }),
378
- canceled: attr("boolean", { optional: true })
379
- }
380
- };
545
+ // src/schema-applications.ts
546
+ var attr = (type, flags = {}) => ({
547
+ type,
548
+ unique: flags.unique ?? false,
549
+ indexed: flags.indexed ?? false,
550
+ optional: flags.optional ?? false
551
+ });
381
552
  var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
382
553
  "firstName",
383
554
  "lastName",
@@ -391,25 +562,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
391
562
  "phone",
392
563
  "state"
393
564
  ]);
565
+ var applicationAttrs = {
566
+ id: attr("string", { unique: true, indexed: true }),
567
+ firstName: attr("string"),
568
+ lastName: attr("string"),
569
+ email: attr("string", { indexed: true }),
570
+ referral: attr("string"),
571
+ referralName: attr("string", { optional: true }),
572
+ whoYouAre: attr("string"),
573
+ focus: attr("json"),
574
+ linkedin: attr("string", { optional: true }),
575
+ message: attr("string"),
576
+ status: attr("string", { indexed: true }),
577
+ createdAt: attr("number", { indexed: true }),
578
+ meetingAt: attr("number", { indexed: true, optional: true }),
579
+ meetingLink: attr("string", { optional: true }),
580
+ clerkUserId: attr("string", { indexed: true, optional: true }),
581
+ clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
582
+ phone: attr("string", { optional: true }),
583
+ state: attr("string", { optional: true }),
584
+ groupId: attr("string", { indexed: true, optional: true }),
585
+ tierId: attr("string", { indexed: true, optional: true }),
586
+ stripeCustomerId: attr("string", { indexed: true, optional: true }),
587
+ stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
588
+ renewalAt: attr("number", { optional: true }),
589
+ disclaimerAckAt: attr("number", { optional: true }),
590
+ refundPolicyAckAt: attr("number", { optional: true }),
591
+ prepEmailSentAt: attr("number", { optional: true }),
592
+ canceled: attr("boolean", { optional: true }),
593
+ emailNormalized: attr("string", { indexed: true, optional: true }),
594
+ admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
595
+ admissionSource: attr("string", { indexed: true, optional: true }),
596
+ interviewWaivedAt: attr("number", { optional: true }),
597
+ giftOfferId: attr("string", { optional: true, indexed: true }),
598
+ giftIntentId: attr("string", { optional: true, unique: true, indexed: true }),
599
+ giftClaimId: attr("string", { optional: true, unique: true, indexed: true }),
600
+ giftOrderId: attr("string", { optional: true, indexed: true }),
601
+ giftPaidAt: attr("number", { optional: true, indexed: true }),
602
+ giftRevokedAt: attr("number", { optional: true, indexed: true }),
603
+ giftCheckoutStartedAt: attr("number", { optional: true, indexed: true })
604
+ };
394
605
  function applicationsFor(application) {
395
606
  const attrs = {};
396
- for (const [name, spec] of Object.entries(applications.attrs)) {
397
- attrs[name] = {
398
- ...spec,
399
- optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
400
- };
607
+ for (const [name, spec] of Object.entries(applicationAttrs)) {
608
+ attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
401
609
  }
402
610
  const apply = (field, optional) => {
403
611
  const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
404
- if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
405
- throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
406
- }
407
- if (existing && existing.type !== "string") {
408
- throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
409
- }
410
- const spec = existing ? { ...existing, optional } : attr("string", { optional });
612
+ if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
613
+ if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
411
614
  Object.defineProperty(attrs, field, {
412
- value: spec,
615
+ value: existing ? { ...existing, optional } : attr("string", { optional }),
413
616
  enumerable: true,
414
617
  configurable: true,
415
618
  writable: true
@@ -419,78 +622,297 @@ function applicationsFor(application) {
419
622
  for (const field of application.optional) apply(field, true);
420
623
  return { attrs };
421
624
  }
422
- var groups = {
625
+
626
+ // src/schema-admission.ts
627
+ var attr2 = (type, flags = {}) => ({
628
+ type,
629
+ unique: flags.unique ?? false,
630
+ indexed: flags.indexed ?? false,
631
+ optional: flags.optional ?? false
632
+ });
633
+ var id = () => attr2("string", { unique: true, indexed: true });
634
+ var admissionGrantsEntity = {
423
635
  attrs: {
424
636
  id: id(),
425
- name: attr("string"),
426
- standardPriceCents: attr("number"),
427
- foundingDiscountCents: attr("number"),
428
- stripePriceId: attr("string", { optional: true }),
429
- stripePublishableKey: attr("string", { optional: true }),
430
- notificationEmail: attr("string"),
431
- replyTo: attr("string"),
432
- debugEmail: attr("string", { optional: true }),
433
- calendarLink: attr("string", { optional: true }),
434
- disclaimerText: attr("string"),
435
- refundPolicyText: attr("string"),
436
- trustCopy: attr("string"),
437
- commitmentText: attr("string", { optional: true }),
438
- normsText: attr("string", { optional: true }),
439
- emailTemplates: attr("json"),
440
- schedulingJson: attr("json", { optional: true }),
441
- createdAt: attr("number", { indexed: true })
637
+ sourceGrantId: attr2("string", { indexed: true }),
638
+ sourceScope: attr2("string", { indexed: true }),
639
+ sourceId: attr2("string", { indexed: true }),
640
+ targetId: attr2("string", { indexed: true }),
641
+ email: attr2("string"),
642
+ emailNormalized: attr2("string", { indexed: true }),
643
+ decision: attr2("string", { indexed: true }),
644
+ note: attr2("string", { optional: true }),
645
+ active: attr2("boolean", { indexed: true }),
646
+ sourceRevision: attr2("number", { optional: true, indexed: true }),
647
+ createdAt: attr2("number", { indexed: true }),
648
+ createdBy: attr2("string", { optional: true, indexed: true }),
649
+ updatedAt: attr2("number", { optional: true, indexed: true }),
650
+ updatedBy: attr2("string", { optional: true, indexed: true }),
651
+ expiresAt: attr2("number", { optional: true, indexed: true }),
652
+ revokedAt: attr2("number", { optional: true, indexed: true }),
653
+ revokedBy: attr2("string", { optional: true, indexed: true }),
654
+ consumedAt: attr2("number", { optional: true, indexed: true }),
655
+ consumedApplicationId: attr2("string", { optional: true, indexed: true })
442
656
  }
443
657
  };
444
- var tiers = {
658
+ var admissionGrantRevisionsEntity = {
445
659
  attrs: {
446
660
  id: id(),
447
- groupId: attr("string", { indexed: true }),
448
- name: attr("string"),
449
- priceCents: attr("number"),
450
- stripePriceId: attr("string", { optional: true }),
451
- blurb: attr("string", { optional: true }),
452
- sortOrder: attr("number", { indexed: true }),
453
- active: attr("boolean", { indexed: true }),
454
- createdAt: attr("number", { indexed: true })
661
+ sourceId: attr2("string", { indexed: true }),
662
+ targetId: attr2("string", { indexed: true }),
663
+ environment: attr2("string", { indexed: true }),
664
+ revision: attr2("number", { indexed: true }),
665
+ digest: attr2("string", { indexed: true }),
666
+ active: attr2("boolean", { indexed: true }),
667
+ receivedAt: attr2("number", { indexed: true })
668
+ }
669
+ };
670
+
671
+ // src/schema-gifts.ts
672
+ var attr3 = (type, flags = {}) => ({
673
+ type,
674
+ unique: flags.unique ?? false,
675
+ indexed: flags.indexed ?? false,
676
+ optional: flags.optional ?? false
677
+ });
678
+ var id2 = () => attr3("string", { unique: true, indexed: true });
679
+ var giftPurchaseIntentsEntity = { attrs: {
680
+ id: id2(),
681
+ chapterId: attr3("string", { indexed: true }),
682
+ environment: attr3("string", { indexed: true }),
683
+ purchaserType: attr3("string", { indexed: true }),
684
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
685
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
686
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
687
+ offerId: attr3("string", { indexed: true }),
688
+ offerSnapshot: attr3("json"),
689
+ status: attr3("string", { indexed: true }),
690
+ codeHash: attr3("string", { indexed: true }),
691
+ codeVersion: attr3("number"),
692
+ codeIssuedAt: attr3("number"),
693
+ providerObjectId: attr3("string", { optional: true, unique: true, indexed: true }),
694
+ providerCustomerId: attr3("string", { optional: true, indexed: true }),
695
+ checkoutStartedAt: attr3("number", { optional: true }),
696
+ paidAt: attr3("number", { optional: true, indexed: true }),
697
+ claimId: attr3("string", { optional: true, unique: true, indexed: true }),
698
+ createdAt: attr3("number", { indexed: true }),
699
+ updatedAt: attr3("number", { indexed: true })
700
+ } };
701
+ var giftOrdersEntity = { attrs: {
702
+ id: id2(),
703
+ intentId: attr3("string", { unique: true, indexed: true }),
704
+ chapterId: attr3("string", { indexed: true }),
705
+ environment: attr3("string", { indexed: true }),
706
+ offerId: attr3("string", { indexed: true }),
707
+ offerSnapshot: attr3("json"),
708
+ purchaserType: attr3("string", { indexed: true }),
709
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
710
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
711
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
712
+ providerEventId: attr3("string", { unique: true, indexed: true }),
713
+ providerObjectId: attr3("string", { indexed: true }),
714
+ amountCents: attr3("number"),
715
+ currency: attr3("string"),
716
+ billingMode: attr3("string"),
717
+ createdAt: attr3("number", { indexed: true })
718
+ } };
719
+ var giftClaimsEntity = { attrs: {
720
+ id: id2(),
721
+ orderId: attr3("string", { unique: true, indexed: true }),
722
+ intentId: attr3("string", { unique: true, indexed: true }),
723
+ chapterId: attr3("string", { indexed: true }),
724
+ offerId: attr3("string", { indexed: true }),
725
+ target: attr3("json"),
726
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
727
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
728
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
729
+ codeHash: attr3("string", { unique: true, indexed: true }),
730
+ codeVersion: attr3("number"),
731
+ rotationCount: attr3("number"),
732
+ codeIssuedAt: attr3("number"),
733
+ codeExpiresAt: attr3("number", { indexed: true }),
734
+ attemptCount: attr3("number"),
735
+ attemptWindowStartedAt: attr3("number"),
736
+ status: attr3("string", { indexed: true }),
737
+ redeemedAt: attr3("number", { optional: true, indexed: true }),
738
+ beneficiaryEmailNormalized: attr3("string", { optional: true, indexed: true }),
739
+ beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
740
+ applicationId: attr3("string", { optional: true, unique: true, indexed: true }),
741
+ entitlementId: attr3("string", { optional: true, unique: true, indexed: true }),
742
+ rotatedAt: attr3("number", { optional: true }),
743
+ revokedAt: attr3("number", { optional: true, indexed: true }),
744
+ revokedReason: attr3("string", { optional: true }),
745
+ createdAt: attr3("number", { indexed: true })
746
+ } };
747
+ var giftEntitlementsEntity = { attrs: {
748
+ id: id2(),
749
+ claimId: attr3("string", { unique: true, indexed: true }),
750
+ orderId: attr3("string", { indexed: true }),
751
+ chapterId: attr3("string", { indexed: true }),
752
+ entitlementKey: attr3("string", { indexed: true }),
753
+ beneficiaryEmailNormalized: attr3("string", { indexed: true }),
754
+ beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
755
+ active: attr3("boolean", { indexed: true }),
756
+ createdAt: attr3("number", { indexed: true }),
757
+ revokedAt: attr3("number", { optional: true })
758
+ } };
759
+ var giftRedemptionAttemptsEntity = { attrs: {
760
+ id: id2(),
761
+ chapterId: attr3("string", { indexed: true }),
762
+ bucket: attr3("string", { indexed: true }),
763
+ attemptCount: attr3("number"),
764
+ windowStartedAt: attr3("number", { indexed: true }),
765
+ updatedAt: attr3("number")
766
+ } };
767
+
768
+ // src/schema.ts
769
+ function attr4(type, flags = {}) {
770
+ return {
771
+ type,
772
+ unique: flags.unique ?? false,
773
+ indexed: flags.indexed ?? false,
774
+ optional: flags.optional ?? false
775
+ };
776
+ }
777
+ var id3 = () => attr4("string", { unique: true, indexed: true });
778
+ var admins = {
779
+ attrs: {
780
+ id: id3(),
781
+ email: attr4("string", { unique: true, indexed: true }),
782
+ name: attr4("string", { optional: true }),
783
+ note: attr4("string", { optional: true })
784
+ }
785
+ };
786
+ var superAdmins = {
787
+ attrs: {
788
+ id: id3(),
789
+ email: attr4("string", { unique: true, indexed: true }),
790
+ note: attr4("string", { optional: true }),
791
+ createdAt: attr4("number", { indexed: true })
792
+ }
793
+ };
794
+ var networkNotes = {
795
+ attrs: {
796
+ id: id3(),
797
+ recordKey: attr4("string", { indexed: true }),
798
+ targetId: attr4("string", { indexed: true }),
799
+ recordType: attr4("string", { indexed: true }),
800
+ recordId: attr4("string", { indexed: true }),
801
+ body: attr4("string"),
802
+ authorId: attr4("string", { indexed: true }),
803
+ authorEmail: attr4("string", { optional: true }),
804
+ createdAt: attr4("number", { indexed: true })
805
+ }
806
+ };
807
+ var signupControls = {
808
+ attrs: {
809
+ id: id3(),
810
+ sourceId: attr4("string", { indexed: true }),
811
+ targetId: attr4("string", { indexed: true }),
812
+ environment: attr4("string", { indexed: true }),
813
+ revision: attr4("number", { indexed: true }),
814
+ digest: attr4("string", { indexed: true }),
815
+ document: attr4("json"),
816
+ active: attr4("boolean", { indexed: true }),
817
+ receivedAt: attr4("number", { indexed: true })
818
+ }
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
+ };
844
+ var groups = {
845
+ attrs: {
846
+ id: id3(),
847
+ name: attr4("string"),
848
+ standardPriceCents: attr4("number"),
849
+ foundingDiscountCents: attr4("number"),
850
+ stripePriceId: attr4("string", { optional: true }),
851
+ stripePublishableKey: attr4("string", { optional: true }),
852
+ notificationEmail: attr4("string"),
853
+ replyTo: attr4("string"),
854
+ debugEmail: attr4("string", { optional: true }),
855
+ calendarLink: attr4("string", { optional: true }),
856
+ disclaimerText: attr4("string"),
857
+ refundPolicyText: attr4("string"),
858
+ trustCopy: attr4("string"),
859
+ commitmentText: attr4("string", { optional: true }),
860
+ normsText: attr4("string", { optional: true }),
861
+ emailTemplates: attr4("json"),
862
+ schedulingJson: attr4("json", { optional: true }),
863
+ createdAt: attr4("number", { indexed: true })
864
+ }
865
+ };
866
+ var tiers = {
867
+ attrs: {
868
+ id: id3(),
869
+ groupId: attr4("string", { indexed: true }),
870
+ name: attr4("string"),
871
+ priceCents: attr4("number"),
872
+ stripePriceId: attr4("string", { optional: true }),
873
+ blurb: attr4("string", { optional: true }),
874
+ sortOrder: attr4("number", { indexed: true }),
875
+ active: attr4("boolean", { indexed: true }),
876
+ createdAt: attr4("number", { indexed: true })
455
877
  }
456
878
  };
457
879
  var meetings = {
458
880
  attrs: {
459
- id: id(),
460
- applicationId: attr("string", { indexed: true }),
461
- groupId: attr("string", { indexed: true }),
462
- startAt: attr("number", { indexed: true }),
463
- endAt: attr("number"),
464
- timezone: attr("string"),
465
- status: attr("string", { indexed: true }),
466
- googleEventId: attr("string", { indexed: true, optional: true }),
467
- meetUrl: attr("string", { optional: true }),
468
- htmlLink: attr("string", { optional: true }),
469
- drift: attr("string", { indexed: true, optional: true }),
470
- driftGoogleStartAt: attr("number", { optional: true }),
471
- driftDetectedAt: attr("number", { optional: true }),
472
- adoptedFromGoogleAt: attr("number", { optional: true }),
473
- createdAt: attr("number", { indexed: true })
881
+ id: id3(),
882
+ applicationId: attr4("string", { indexed: true }),
883
+ groupId: attr4("string", { indexed: true }),
884
+ startAt: attr4("number", { indexed: true }),
885
+ endAt: attr4("number"),
886
+ timezone: attr4("string"),
887
+ status: attr4("string", { indexed: true }),
888
+ googleEventId: attr4("string", { indexed: true, optional: true }),
889
+ meetUrl: attr4("string", { optional: true }),
890
+ htmlLink: attr4("string", { optional: true }),
891
+ drift: attr4("string", { indexed: true, optional: true }),
892
+ driftGoogleStartAt: attr4("number", { optional: true }),
893
+ driftDetectedAt: attr4("number", { optional: true }),
894
+ adoptedFromGoogleAt: attr4("number", { optional: true }),
895
+ createdAt: attr4("number", { indexed: true })
474
896
  }
475
897
  };
476
898
  var emailLog = {
477
899
  attrs: {
478
- id: id(),
479
- groupId: attr("string", { indexed: true }),
480
- applicationId: attr("string", { indexed: true, optional: true }),
481
- to: attr("string", { indexed: true }),
482
- template: attr("string", { indexed: true }),
483
- subject: attr("string"),
484
- body: attr("string", { optional: true }),
485
- transport: attr("string"),
486
- messageId: attr("string", { optional: true }),
487
- redirected: attr("boolean", { optional: true }),
488
- dedupeKey: attr("string", { indexed: true, optional: true }),
489
- error: attr("string", { optional: true }),
490
- sentAt: attr("number", { indexed: true })
900
+ id: id3(),
901
+ groupId: attr4("string", { indexed: true }),
902
+ applicationId: attr4("string", { indexed: true, optional: true }),
903
+ to: attr4("string", { indexed: true }),
904
+ template: attr4("string", { indexed: true }),
905
+ subject: attr4("string"),
906
+ body: attr4("string", { optional: true }),
907
+ transport: attr4("string"),
908
+ messageId: attr4("string", { optional: true }),
909
+ redirected: attr4("boolean", { optional: true }),
910
+ dedupeKey: attr4("string", { indexed: true, optional: true }),
911
+ error: attr4("string", { optional: true }),
912
+ sentAt: attr4("number", { indexed: true })
491
913
  }
492
914
  };
493
- 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) {
494
916
  const entities = {};
495
917
  if (mode === "chapter") {
496
918
  entities.applications = applicationsFor(application);
@@ -498,7 +920,18 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
498
920
  entities.tiers = tiers;
499
921
  entities.meetings = meetings;
500
922
  entities.emailLog = emailLog;
923
+ entities.admissionGrants = admissionGrantsEntity;
924
+ entities.admissionGrantRevisions = admissionGrantRevisionsEntity;
925
+ entities.giftPurchaseIntents = giftPurchaseIntentsEntity;
926
+ entities.giftOrders = giftOrdersEntity;
927
+ entities.giftClaims = giftClaimsEntity;
928
+ entities.giftEntitlements = giftEntitlementsEntity;
929
+ entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
501
930
  if (includeSignupControl) entities.signupControls = signupControls;
931
+ if (includeMemberContent) {
932
+ entities.memberContentControls = memberContentControls;
933
+ entities.memberContentLocal = memberContentLocal;
934
+ }
502
935
  }
503
936
  if (auth.source === "table") entities.admins = admins;
504
937
  if (auth.superAdmins) entities.superAdmins = superAdmins;
@@ -782,6 +1215,7 @@ var DEFAULT_ADMIN_COPY = {
782
1215
  overview: "Overview",
783
1216
  billing: "Billing",
784
1217
  people: "People",
1218
+ admission: "Admission",
785
1219
  network: "Network",
786
1220
  portfolio: "Portfolio",
787
1221
  settings: "Settings",
@@ -892,6 +1326,26 @@ var DEFAULT_ADMIN_COPY = {
892
1326
  reschedule: "Reschedule",
893
1327
  cancel: "Cancel"
894
1328
  },
1329
+ admission: {
1330
+ title: "Interview decisions",
1331
+ description: "Local decisions override Parent decisions. Requiring an interview locally suppresses a Parent waiver for the same email.",
1332
+ email: "Email",
1333
+ decision: "Decision",
1334
+ expires: "Expires",
1335
+ note: "Internal note",
1336
+ waive: "Waive interview",
1337
+ require: "Require interview",
1338
+ add: "Add decision",
1339
+ ledger: "Effective grant ledger",
1340
+ loading: "Loading\u2026",
1341
+ consumed: "consumed",
1342
+ revoked: "revoked",
1343
+ expired: "expired",
1344
+ active: "active",
1345
+ local: "local",
1346
+ parent: "Parent",
1347
+ revoke: "Revoke"
1348
+ },
895
1349
  network: {
896
1350
  title: "Share to a follower",
897
1351
  allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
@@ -990,6 +1444,19 @@ var DEFAULT_JOIN_COPY = {
990
1444
  label: "You're booked",
991
1445
  calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
992
1446
  memberArea: "Go to your member area"
1447
+ },
1448
+ accepted: {
1449
+ label: "You're approved",
1450
+ body: "Your membership is approved. No introduction call is required."
1451
+ },
1452
+ gift: {
1453
+ chooseSelf: "Buy membership for myself",
1454
+ optionsLabel: "Or give access to someone else",
1455
+ label: "Your gift code",
1456
+ body: "Share this one-time code with the recipient. It becomes active after payment is confirmed.",
1457
+ save: "Save it now. For security, this code is shown only once.",
1458
+ rotate: "Generate a new code",
1459
+ rotateFailed: "A new code could not be generated."
993
1460
  }
994
1461
  };
995
1462
  var DEFAULT_MEMBERS_COPY = {
@@ -1017,6 +1484,26 @@ var DEFAULT_MEMBERS_COPY = {
1017
1484
  full: {
1018
1485
  welcome: "Welcome back."
1019
1486
  },
1487
+ gifts: {
1488
+ title: "Give access",
1489
+ body: "Purchase access for someone else and share the one-time claim code.",
1490
+ noneAvailable: "No gift options are available right now.",
1491
+ paid: "Payment submitted. Save the code below while it is visible.",
1492
+ saveCode: "This code is shown once. Save it before leaving this page.",
1493
+ purchases: "Your gifts",
1494
+ noPurchases: "No gift purchases yet.",
1495
+ pending: "Pending",
1496
+ redeemed: "Redeemed",
1497
+ expired: "Expired",
1498
+ revoked: "Revoked",
1499
+ rotate: "Rotate code",
1500
+ rotated: "A new code was generated. Save it now.",
1501
+ rotateFailed: "The code could not be rotated.",
1502
+ redeemTitle: "Redeem a gift",
1503
+ code: "One-time code",
1504
+ redeem: "Redeem",
1505
+ redeemFailed: "The code is invalid or unavailable."
1506
+ },
1020
1507
  reschedule: {
1021
1508
  changeTime: "Change your time",
1022
1509
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
@@ -1132,13 +1619,13 @@ function resolveLeaderFormation(input, crm) {
1132
1619
  }
1133
1620
  function formationFields(crm, formation) {
1134
1621
  const def = crm.type(formation.type);
1135
- return [...formation.required, ...formation.optional].map((id2) => {
1136
- const field = def.fields[id2];
1622
+ return [...formation.required, ...formation.optional].map((id4) => {
1623
+ const field = def.fields[id4];
1137
1624
  return {
1138
- id: id2,
1139
- label: field.label ?? id2,
1625
+ id: id4,
1626
+ label: field.label ?? id4,
1140
1627
  type: field.type,
1141
- required: formation.required.includes(id2),
1628
+ required: formation.required.includes(id4),
1142
1629
  ...field.options ? { options: field.options } : {}
1143
1630
  };
1144
1631
  });
@@ -1290,16 +1777,36 @@ function resolveSignupControl(input) {
1290
1777
  return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
1291
1778
  }
1292
1779
 
1293
- // src/config.ts
1780
+ // src/config-member-content.ts
1294
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}$/;
1295
1802
  function isResolvedCrm(x) {
1296
1803
  return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
1297
1804
  }
1298
1805
  function defineChapter(config) {
1299
1806
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
1300
- const { id: id2, name } = config;
1301
- if (typeof id2 !== "string" || !SLUG3.test(id2)) {
1302
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
1807
+ const { id: id4, name } = config;
1808
+ if (typeof id4 !== "string" || !SLUG4.test(id4)) {
1809
+ throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
1303
1810
  }
1304
1811
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
1305
1812
  const mode = config.mode ?? "chapter";
@@ -1323,16 +1830,21 @@ function defineChapter(config) {
1323
1830
  const copy = resolveChapterCopy(config.copy);
1324
1831
  const network = resolveNetwork(config, crm);
1325
1832
  const signupControl = resolveSignupControl(config.signupControl);
1833
+ const memberContent = resolveMemberContent(config.memberContent);
1326
1834
  const formation = resolveLeaderFormation(config.formation, crm);
1327
1835
  if (signupControl && mode !== "chapter") {
1328
1836
  throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
1329
1837
  }
1838
+ if (memberContent && mode !== "chapter") {
1839
+ throw new Error("defineChapter.memberContent: only chapter-mode sites can serve member content");
1840
+ }
1330
1841
  const { schema, rules } = chapterDb(
1331
1842
  mode,
1332
1843
  auth,
1333
1844
  network.targets.length > 0,
1334
1845
  application,
1335
- Boolean(signupControl)
1846
+ Boolean(signupControl),
1847
+ Boolean(memberContent)
1336
1848
  );
1337
1849
  const services = config.services ?? ["db", "calendar", "o11y"];
1338
1850
  const account = config.account ?? "none";
@@ -1382,13 +1894,14 @@ function defineChapter(config) {
1382
1894
  };
1383
1895
  const chapter = {
1384
1896
  config,
1385
- id: id2,
1897
+ id: id4,
1386
1898
  name,
1387
1899
  mode,
1388
1900
  brand,
1389
1901
  copy,
1390
1902
  network,
1391
1903
  signupControl,
1904
+ memberContent,
1392
1905
  formation,
1393
1906
  crm,
1394
1907
  auth,
@@ -1596,8 +2109,8 @@ function tierPayable(tier, group, hasSecretKey) {
1596
2109
  function byOrder(a, b) {
1597
2110
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1598
2111
  }
1599
- function resolveTiers(group, rows = []) {
1600
- const stored = rows.filter((tier) => tier.active);
2112
+ function resolveTiers(group, rows2 = []) {
2113
+ const stored = rows2.filter((tier) => tier.active);
1601
2114
  if (stored.length) return [...stored].sort(byOrder);
1602
2115
  return [{
1603
2116
  id: "standard",
@@ -1683,12 +2196,332 @@ function createChapterIntegration(chapter, options = {}) {
1683
2196
  }] : [],
1684
2197
  // Formation metadata is public only when the host explicitly enables
1685
2198
  // the bounded intake contract.
1686
- ...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
+ ] : []
1687
2204
  ],
1688
2205
  runbooks
1689
2206
  };
1690
2207
  }
1691
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
+
1692
2525
  // src/email.ts
1693
2526
  function render(template, vars) {
1694
2527
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
@@ -1719,10 +2552,10 @@ function planDelivery(input) {
1719
2552
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1720
2553
  const to = redirect ? input.group.debugEmail : input.to;
1721
2554
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1722
- const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
2555
+ const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
1723
2556
 
1724
2557
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1725
- return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
2558
+ return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
1726
2559
  }
1727
2560
 
1728
2561
  // src/notify.ts
@@ -1757,9 +2590,9 @@ async function sendTemplated(deps, input) {
1757
2590
  error = e instanceof Error ? e.message : String(e);
1758
2591
  }
1759
2592
  }
1760
- const id2 = deps.newId();
2593
+ const id4 = deps.newId();
1761
2594
  const row = {
1762
- id: id2,
2595
+ id: id4,
1763
2596
  groupId: input.group.id,
1764
2597
  to: decision.to,
1765
2598
  template: input.template,
@@ -1773,7 +2606,7 @@ async function sendTemplated(deps, input) {
1773
2606
  ...messageId ? { messageId } : {},
1774
2607
  ...error ? { error } : {}
1775
2608
  };
1776
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id2, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
2609
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id4, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
1777
2610
  return error ? { sent: false, reason: error } : { sent: true };
1778
2611
  }
1779
2612
  function emailGroupFrom(row) {
@@ -1809,8 +2642,26 @@ function findApplicationRef(obj) {
1809
2642
  const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
1810
2643
  return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
1811
2644
  }
2645
+ function findGiftRef(obj) {
2646
+ const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
2647
+ const pick = (metadata) => typeof metadata.giftIntentId === "string" ? metadata.giftIntentId : void 0;
2648
+ const parent = obj.parent;
2649
+ const subscription = obj.subscription_details ?? parent?.subscription_details;
2650
+ const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
2651
+ const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
2652
+ return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
2653
+ }
1812
2654
  function normalizeWebhookEvent(event) {
1813
2655
  const obj = event.data?.object ?? {};
2656
+ const gift = findGiftRef(obj);
2657
+ if (gift.giftIntentId) {
2658
+ if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
2659
+ return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
2660
+ }
2661
+ if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
2662
+ if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
2663
+ return { kind: "ignored", type: event.type };
2664
+ }
1814
2665
  const ref = findApplicationRef(obj);
1815
2666
  switch (event.type) {
1816
2667
  case "invoice.paid": {
@@ -1844,6 +2695,112 @@ function canceledPatch() {
1844
2695
  return { canceled: true };
1845
2696
  }
1846
2697
 
2698
+ // src/join-continuation.ts
2699
+ function resolveJoinContinuation(input) {
2700
+ if (input.status === "approved") return "accepted";
2701
+ if (input.interviewWaived) {
2702
+ if (!input.payable) return "approve";
2703
+ if (input.status === input.initialStatus) {
2704
+ return input.subscriptionStarted ? "paymentPending" : "payment";
2705
+ }
2706
+ return "approve";
2707
+ }
2708
+ if (!input.payable) return "booking";
2709
+ if (input.status === input.initialStatus) {
2710
+ return input.subscriptionStarted ? "paymentPending" : "payment";
2711
+ }
2712
+ if (canBook(input.status, input.pipeline)) return "booking";
2713
+ return null;
2714
+ }
2715
+
2716
+ // src/gift-offers.ts
2717
+ var GIFT_OFFER_CATALOG_VERSION = 1;
2718
+ var MAX_GIFT_OFFERS = 20;
2719
+ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
2720
+ var PRICE = /^price_[A-Za-z0-9]{12,}$/;
2721
+ var CURRENCY = /^[a-z]{3}$/;
2722
+ var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
2723
+ var text = (value, path, errors, max) => {
2724
+ if (typeof value !== "string" || !value.trim()) {
2725
+ errors.push(`${path} is required`);
2726
+ return "";
2727
+ }
2728
+ if (value.trim().length > max) errors.push(`${path} must be at most ${max} characters`);
2729
+ return value.trim();
2730
+ };
2731
+ function validateGiftOfferCatalog(input, tierIds) {
2732
+ const errors = [];
2733
+ const root = record2(input);
2734
+ if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
2735
+ for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
2736
+ if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
2737
+ if (!Array.isArray(root.offers)) errors.push("document.giftOffers.offers must be an array");
2738
+ const rawOffers = Array.isArray(root.offers) ? root.offers : [];
2739
+ if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
2740
+ const ids = /* @__PURE__ */ new Set();
2741
+ const offers = [];
2742
+ for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
2743
+ const path = `document.giftOffers.offers[${index}]`;
2744
+ const item = record2(raw);
2745
+ if (!item) {
2746
+ errors.push(`${path} must be an object`);
2747
+ continue;
2748
+ }
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`);
2751
+ const id4 = text(item.id, `${path}.id`, errors, 80);
2752
+ if (id4 && !ID3.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2753
+ if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
2754
+ ids.add(id4);
2755
+ const targetRaw = record2(item.target);
2756
+ let target = { type: "entitlement", entitlementKey: "invalid" };
2757
+ if (targetRaw?.type === "tier") {
2758
+ const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
2759
+ if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
2760
+ target = { type: "tier", tierId };
2761
+ } else if (targetRaw?.type === "entitlement") {
2762
+ const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
2763
+ if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
2764
+ target = { type: "entitlement", entitlementKey };
2765
+ } else errors.push(`${path}.target.type must be tier or entitlement`);
2766
+ if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
2767
+ const stripePriceId = text(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
2768
+ if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2769
+ if (!Number.isSafeInteger(item.priceCents) || Number(item.priceCents) < 1 || Number(item.priceCents) > 1e8) errors.push(`${path}.priceCents must be an integer from 1 to 100,000,000`);
2770
+ const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
2771
+ if (!CURRENCY.test(currency)) errors.push(`${path}.currency must be a lowercase ISO currency code`);
2772
+ const lifetime = Number(item.redemptionLifetimeHours);
2773
+ if (!Number.isSafeInteger(lifetime) || lifetime < 1 || lifetime > 8760) errors.push(`${path}.redemptionLifetimeHours must be an integer from 1 to 8760`);
2774
+ const activeFrom = item.activeFrom === void 0 ? void 0 : Number(item.activeFrom);
2775
+ const activeUntil = item.activeUntil === void 0 ? void 0 : Number(item.activeUntil);
2776
+ if (activeFrom !== void 0 && (!Number.isSafeInteger(activeFrom) || activeFrom < 1)) errors.push(`${path}.activeFrom must be epoch milliseconds`);
2777
+ if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
2778
+ if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
2779
+ offers.push({
2780
+ id: id4,
2781
+ target,
2782
+ billingMode: item.billingMode,
2783
+ stripePriceId,
2784
+ priceCents: Number(item.priceCents),
2785
+ currency,
2786
+ redemptionLifetimeHours: lifetime,
2787
+ ...activeFrom !== void 0 ? { activeFrom } : {},
2788
+ ...activeUntil !== void 0 ? { activeUntil } : {},
2789
+ title: text(item.title, `${path}.title`, errors, 160),
2790
+ description: text(item.description, `${path}.description`, errors, 2e3),
2791
+ ctaLabel: text(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
2792
+ });
2793
+ }
2794
+ return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
2795
+ }
2796
+ var giftOfferActive = (offer, now) => (!offer.activeFrom || offer.activeFrom <= now) && (!offer.activeUntil || offer.activeUntil > now);
2797
+ function publicGiftOffers(catalog, now) {
2798
+ return (catalog?.offers ?? []).filter((offer) => giftOfferActive(offer, now)).map(({ stripePriceId: _stripe, target: _target, ...offer }) => offer);
2799
+ }
2800
+ function findGiftOffer(catalog, offerId, now) {
2801
+ return catalog?.offers.find((offer) => offer.id === offerId && giftOfferActive(offer, now)) ?? null;
2802
+ }
2803
+
1847
2804
  // src/network.ts
1848
2805
  var import_crm5 = require("@odla-ai/crm");
1849
2806
 
@@ -1883,34 +2840,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
1883
2840
  if (readable && raw.length <= 64) return raw;
1884
2841
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
1885
2842
  }
1886
- function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1887
- if ("input" in record && record.version === 2) {
2843
+ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
2844
+ if ("input" in record3 && record3.version === 2) {
1888
2845
  return {
1889
2846
  version: 2,
1890
- sourceId: record.source.siteId,
1891
- sourceRecordId: record.source.recordId,
1892
- type: record.type,
1893
- input: record.input
2847
+ sourceId: record3.source.siteId,
2848
+ sourceRecordId: record3.source.recordId,
2849
+ type: record3.type,
2850
+ input: record3.input
1894
2851
  };
1895
2852
  }
1896
- if ("input" in record) {
2853
+ if ("input" in record3) {
1897
2854
  return {
1898
2855
  version: 1,
1899
2856
  sourceId: fallbackSourceId,
1900
- sourceRecordId: record.hubRecordId,
1901
- type: record.type,
1902
- input: record.input
2857
+ sourceRecordId: record3.hubRecordId,
2858
+ type: record3.type,
2859
+ input: record3.input
1903
2860
  };
1904
2861
  }
1905
- if ("type" in record && record.type === "company") {
1906
- const input = { name: record.name };
2862
+ if ("type" in record3 && record3.type === "company") {
2863
+ const input = { name: record3.name };
1907
2864
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
1908
- if (record[key]) input[key] = record[key];
2865
+ if (record3[key]) input[key] = record3[key];
1909
2866
  }
1910
2867
  return {
1911
2868
  version: 1,
1912
2869
  sourceId: fallbackSourceId,
1913
- sourceRecordId: record.hubRecordId,
2870
+ sourceRecordId: record3.hubRecordId,
1914
2871
  type: "company",
1915
2872
  input
1916
2873
  };
@@ -1918,28 +2875,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1918
2875
  return {
1919
2876
  version: 1,
1920
2877
  sourceId: fallbackSourceId,
1921
- sourceRecordId: record.hubRecordId,
2878
+ sourceRecordId: record3.hubRecordId,
1922
2879
  type: "person",
1923
- input: sharedPersonInput(record)
2880
+ input: sharedPersonInput(record3)
1924
2881
  };
1925
2882
  }
1926
- function sharedRecordFromCrm(crm, record, target, sourceId) {
1927
- if (target.fields && !target.fields[record.type]) {
1928
- throw new Error(`${target.name} does not accept "${record.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`);
1929
2886
  }
1930
- const def = crm.type(record.type);
1931
- const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
2887
+ const def = crm.type(record3.type);
2888
+ const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
1932
2889
  if (!fields) {
1933
- throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
2890
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
1934
2891
  }
1935
2892
  const nameField = def.nameField ?? "name";
1936
2893
  const input = {};
1937
2894
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
1938
- const value = record.fields?.[field];
2895
+ const value = record3.fields?.[field];
1939
2896
  if (value !== void 0) input[field] = value;
1940
2897
  }
1941
- if (input[nameField] === void 0) input[nameField] = record.name;
1942
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.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 };
1943
2900
  }
1944
2901
  async function upsertPerson(deps, opts) {
1945
2902
  const email = opts.email.toLowerCase();
@@ -1953,11 +2910,11 @@ async function upsertPerson(deps, opts) {
1953
2910
  const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
1954
2911
  return { recordId: created.id };
1955
2912
  }
1956
- async function findSharedRecord(deps, record, tag) {
2913
+ async function findSharedRecord(deps, record3, tag) {
1957
2914
  const structured = await (0, import_crm5.getRecordByOrigin)(
1958
2915
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
1959
- record.sourceId,
1960
- record.sourceRecordId
2916
+ record3.sourceId,
2917
+ record3.sourceRecordId
1961
2918
  );
1962
2919
  if (structured) return structured;
1963
2920
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -1966,53 +2923,53 @@ async function findSharedRecord(deps, record, tag) {
1966
2923
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1967
2924
  if (found.crm_record?.[0]) return found.crm_record[0];
1968
2925
  }
1969
- const def = deps.crm.type(record.type);
2926
+ const def = deps.crm.type(record3.type);
1970
2927
  const emailField = def.emailField;
1971
- if (emailField && typeof record.input[emailField] === "string") {
1972
- const primaryEmail = record.input[emailField].toLowerCase();
2928
+ if (emailField && typeof record3.input[emailField] === "string") {
2929
+ const primaryEmail = record3.input[emailField].toLowerCase();
1973
2930
  const found = await deps.db.query({
1974
- crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
2931
+ crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
1975
2932
  });
1976
2933
  if (found.crm_record?.[0]) return found.crm_record[0];
1977
2934
  }
1978
- const domain = record.input.domain;
2935
+ const domain = record3.input.domain;
1979
2936
  const domainSlot = def.fields.domain?.slot;
1980
2937
  if (typeof domain === "string" && domainSlot) {
1981
2938
  const found = await deps.db.query({
1982
- crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
2939
+ crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
1983
2940
  });
1984
2941
  if (found.crm_record?.[0]) return found.crm_record[0];
1985
2942
  }
1986
2943
  const nameField = def.nameField ?? "name";
1987
- const name = record.input[nameField];
1988
- if (record.type === "company" && typeof name === "string" && name.trim()) {
2944
+ const name = record3.input[nameField];
2945
+ if (record3.type === "company" && typeof name === "string" && name.trim()) {
1989
2946
  const found = await deps.db.query({
1990
- crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
2947
+ crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
1991
2948
  });
1992
2949
  if (found.crm_record?.[0]) return found.crm_record[0];
1993
2950
  }
1994
2951
  return void 0;
1995
2952
  }
1996
2953
  async function projectSharedRecord(deps, shared, options = {}) {
1997
- const record = normalizeSharedRecord(shared, options.sourceId);
1998
- if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
2954
+ const record3 = normalizeSharedRecord(shared, options.sourceId);
2955
+ if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
1999
2956
  throw new Error("type, source site id, and source record id must be non-empty");
2000
2957
  }
2001
- if (options.sourceId && options.sourceId !== record.sourceId) {
2958
+ if (options.sourceId && options.sourceId !== record3.sourceId) {
2002
2959
  throw new Error("signed sender does not match payload source");
2003
2960
  }
2004
- const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
2961
+ const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
2005
2962
  const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
2006
- const existing = await findSharedRecord(deps, record, tag);
2963
+ const existing = await findSharedRecord(deps, record3, tag);
2007
2964
  let recordId;
2008
2965
  if (existing && typeof existing.id === "string") {
2009
- await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record.input });
2966
+ await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record3.input });
2010
2967
  recordId = existing.id;
2011
2968
  } else {
2012
- recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
2969
+ recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
2013
2970
  await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
2014
- type: record.type,
2015
- input: record.input,
2971
+ type: record3.type,
2972
+ input: record3.input,
2016
2973
  mutationId: `share-create:${tag}`
2017
2974
  });
2018
2975
  }
@@ -2022,10 +2979,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
2022
2979
  );
2023
2980
  await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
2024
2981
  recordId,
2025
- sourceId: record.sourceId,
2026
- sourceRecordId: record.sourceRecordId,
2982
+ sourceId: record3.sourceId,
2983
+ sourceRecordId: record3.sourceRecordId,
2027
2984
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
2028
- payloadVersion: record.version
2985
+ payloadVersion: record3.version
2029
2986
  });
2030
2987
  return { recordId };
2031
2988
  }
@@ -2241,9 +3198,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
2241
3198
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
2242
3199
  if (!found.ok) return false;
2243
3200
  const users = await found.json().catch(() => null);
2244
- const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
2245
- if (!id2) return false;
2246
- return updateClerkUserMetadata(secretKey, id2, input, fetchImpl);
3201
+ const id4 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
3202
+ if (!id4) return false;
3203
+ return updateClerkUserMetadata(secretKey, id4, input, fetchImpl);
2247
3204
  }
2248
3205
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
2249
3206
  const { path, body } = clerkUserRequest(input);
@@ -2299,8 +3256,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
2299
3256
  const user = Array.isArray(data) ? data[0] : void 0;
2300
3257
  return user ? toRecord(user) : null;
2301
3258
  }
2302
- async function clerkGetUser(secretKey, id2, fetchImpl = fetch) {
2303
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id2)}`, secretKey, fetchImpl).catch(() => null);
3259
+ async function clerkGetUser(secretKey, id4, fetchImpl = fetch) {
3260
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id4)}`, secretKey, fetchImpl).catch(() => null);
2304
3261
  return data ? toRecord(data) : null;
2305
3262
  }
2306
3263
  async function clerkListUsers(secretKey, fetchImpl = fetch) {
@@ -2309,15 +3266,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
2309
3266
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
2310
3267
  const page = Array.isArray(data) ? data : [];
2311
3268
  for (const u of page) {
2312
- const record = toRecord(u);
2313
- if (record) out.push(record);
3269
+ const record3 = toRecord(u);
3270
+ if (record3) out.push(record3);
2314
3271
  }
2315
3272
  if (page.length < PAGE) break;
2316
3273
  }
2317
3274
  return out;
2318
3275
  }
2319
- async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
2320
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id2)}/metadata`, {
3276
+ async function clerkSetRole(secretKey, id4, role, fetchImpl = fetch) {
3277
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id4)}/metadata`, {
2321
3278
  method: "PATCH",
2322
3279
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
2323
3280
  body: JSON.stringify({ public_metadata: { role } })
@@ -2412,7 +3369,8 @@ function applicationSummary(app) {
2412
3369
  meetingLink: app.meetingLink ?? null,
2413
3370
  paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
2414
3371
  renewalAt: app.renewalAt ?? null,
2415
- canceled: app.canceled === true
3372
+ canceled: app.canceled === true,
3373
+ tierId: app.tierId ?? null
2416
3374
  };
2417
3375
  }
2418
3376
  function memberApplication(app, meeting, defaultTimezone) {
@@ -2693,15 +3651,15 @@ function rowToTier(row, groupId) {
2693
3651
  }
2694
3652
  async function loadTiers(db, group) {
2695
3653
  const groupId = String(group.id ?? "");
2696
- let rows;
3654
+ let rows2;
2697
3655
  try {
2698
- rows = (await db.query({
3656
+ rows2 = (await db.query({
2699
3657
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
2700
3658
  })).tiers;
2701
3659
  } catch {
2702
- rows = void 0;
3660
+ rows2 = void 0;
2703
3661
  }
2704
- const list = Array.isArray(rows) ? rows.map((row) => rowToTier(row, groupId)) : [];
3662
+ const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
2705
3663
  return resolveTiers(group, list);
2706
3664
  }
2707
3665
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -2712,37 +3670,11 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
2712
3670
  var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
2713
3671
  var SIGNUP_CONTROL_PATH = "/api/signup-control";
2714
3672
 
2715
- // src/signup-control-canonical.ts
2716
- var encoder = new TextEncoder();
2717
- function canonicalValue(value) {
2718
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2719
- return JSON.stringify(value);
2720
- }
2721
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
2722
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
2723
- const record = value;
2724
- const entries = Object.keys(record).sort().flatMap((key) => {
2725
- const item = record[key];
2726
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
2727
- });
2728
- return `{${entries.join(",")}}`;
2729
- }
2730
- function canonicalSignupControl(value) {
2731
- return canonicalValue(value);
2732
- }
2733
- async function signupControlDigest(document) {
2734
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
2735
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2736
- }
2737
- async function createSignupControlEnvelope(document) {
2738
- return { document, digest: await signupControlDigest(document) };
2739
- }
2740
-
2741
3673
  // src/membership-page.ts
2742
3674
  var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
2743
3675
  var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
2744
3676
  var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
2745
- var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
3677
+ var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
2746
3678
  var TIER_KEYS = /* @__PURE__ */ new Set([
2747
3679
  "tierId",
2748
3680
  "variant",
@@ -2761,12 +3693,12 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
2761
3693
  var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
2762
3694
  var MAX_TIERS = 20;
2763
3695
  var MAX_BENEFITS = 24;
2764
- var MAX_MARKDOWN = 65536;
3696
+ var MAX_MARKDOWN2 = 65536;
2765
3697
  var MAX_LABEL = 160;
2766
3698
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2767
- function rejectUnknownKeys(value, allowed, path, errors) {
3699
+ function rejectUnknownKeys(value, allowed2, path, errors) {
2768
3700
  for (const key of Object.keys(value)) {
2769
- if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
3701
+ if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2770
3702
  }
2771
3703
  }
2772
3704
  function boundedString(value, path, errors, max, required = false) {
@@ -2778,16 +3710,16 @@ function boundedString(value, path, errors, max, required = false) {
2778
3710
  errors.push(`${path} must be a string`);
2779
3711
  return void 0;
2780
3712
  }
2781
- const text2 = value.trim();
2782
- if (required && !text2) errors.push(`${path} is required`);
2783
- if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2784
- return text2;
3713
+ const text3 = value.trim();
3714
+ if (required && !text3) errors.push(`${path} is required`);
3715
+ if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
3716
+ return text3;
2785
3717
  }
2786
3718
  function validateMembershipPageDocument(input, options = {}) {
2787
3719
  const errors = [];
2788
3720
  const root = objectRecord(input);
2789
3721
  if (!root) return { ok: false, errors: ["document must be an object"] };
2790
- rejectUnknownKeys(root, ROOT_KEYS, "document", errors);
3722
+ rejectUnknownKeys(root, ROOT_KEYS2, "document", errors);
2791
3723
  if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
2792
3724
  if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
2793
3725
  errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
@@ -2830,8 +3762,8 @@ function validateMembershipPageDocument(input, options = {}) {
2830
3762
  else {
2831
3763
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
2832
3764
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
2833
- const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2834
- return text2 ? [text2] : [];
3765
+ const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
3766
+ return text3 ? [text3] : [];
2835
3767
  });
2836
3768
  }
2837
3769
  }
@@ -2841,7 +3773,7 @@ function validateMembershipPageDocument(input, options = {}) {
2841
3773
  if (!rawCallout) errors.push(`${path}.callout must be an object`);
2842
3774
  else {
2843
3775
  rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
2844
- 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);
2845
3777
  const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
2846
3778
  if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
2847
3779
  }
@@ -2863,10 +3795,10 @@ function validateMembershipPageDocument(input, options = {}) {
2863
3795
  }
2864
3796
  }
2865
3797
  const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
2866
- const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN);
3798
+ const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN2);
2867
3799
  const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
2868
3800
  const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
2869
- const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN);
3801
+ const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN2);
2870
3802
  const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
2871
3803
  tiers2.push({
2872
3804
  tierId,
@@ -2888,7 +3820,7 @@ function validateMembershipPageDocument(input, options = {}) {
2888
3820
  if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
2889
3821
  }
2890
3822
  }
2891
- const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN);
3823
+ const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN2);
2892
3824
  if (errors.length) return { ok: false, errors };
2893
3825
  return {
2894
3826
  ok: true,
@@ -2907,11 +3839,11 @@ function membershipPageDocument(input, options = {}) {
2907
3839
  }
2908
3840
 
2909
3841
  // src/signup-control-validate.ts
2910
- var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
3842
+ var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2911
3843
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
2912
3844
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
2913
- var PRICE = /^price_[A-Za-z0-9]{12,}$/;
2914
- var ROOT_KEYS2 = /* @__PURE__ */ new Set([
3845
+ var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
3846
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set([
2915
3847
  "schemaVersion",
2916
3848
  "revision",
2917
3849
  "generatedAt",
@@ -2922,14 +3854,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2922
3854
  "application",
2923
3855
  "policy",
2924
3856
  "scheduling",
2925
- "theme"
3857
+ "theme",
3858
+ "giftOffers"
2926
3859
  ]);
2927
3860
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2928
3861
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2929
- function unknownKeys(value, allowed, path, errors) {
2930
- 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`);
2931
3864
  }
2932
- function text(value, path, errors, max = 65536) {
3865
+ function text2(value, path, errors, max = 65536) {
2933
3866
  if (typeof value !== "string" || !value.trim()) {
2934
3867
  errors.push(`${path} is required`);
2935
3868
  return "";
@@ -2951,26 +3884,26 @@ function tiersOf(value, errors) {
2951
3884
  errors.push(`${path} must be an object`);
2952
3885
  return [];
2953
3886
  }
2954
- unknownKeys(item, TIER_KEYS2, path, errors);
2955
- const id2 = text(item.id, `${path}.id`, errors, 80);
2956
- if (id2 && !TIER.test(id2)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2957
- if (seen.has(id2)) errors.push(`${path}.id duplicates "${id2}"`);
2958
- seen.add(id2);
2959
- const name = text(item.name, `${path}.name`, errors, 160);
3887
+ unknownKeys2(item, TIER_KEYS2, path, errors);
3888
+ const id4 = text2(item.id, `${path}.id`, errors, 80);
3889
+ if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3890
+ if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
3891
+ seen.add(id4);
3892
+ const name = text2(item.name, `${path}.name`, errors, 160);
2960
3893
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
2961
3894
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
2962
3895
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
2963
3896
  errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
2964
3897
  }
2965
- if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
3898
+ if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
2966
3899
  errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2967
3900
  }
2968
3901
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
2969
3902
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
2970
3903
  }
2971
- const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
3904
+ const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
2972
3905
  return [{
2973
- id: id2,
3906
+ id: id4,
2974
3907
  name,
2975
3908
  available: item.available === true,
2976
3909
  sortOrder: Number(item.sortOrder) || 0,
@@ -2987,7 +3920,7 @@ function applicationOf(value, errors) {
2987
3920
  errors.push("document.application must be an object");
2988
3921
  return void 0;
2989
3922
  }
2990
- 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);
2991
3924
  try {
2992
3925
  const resolved = resolveApplication(input);
2993
3926
  const copy = recordOf(input.copy);
@@ -3012,12 +3945,12 @@ function policyOf(value, errors) {
3012
3945
  errors.push("document.policy must be an object");
3013
3946
  return void 0;
3014
3947
  }
3015
- const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
3016
- unknownKeys(input, allowed, "document.policy", errors);
3948
+ const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
3949
+ unknownKeys2(input, allowed2, "document.policy", errors);
3017
3950
  const result = {};
3018
- for (const key of allowed) {
3951
+ for (const key of allowed2) {
3019
3952
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
3020
- result[key] = text(input[key], `document.policy.${key}`, errors);
3953
+ result[key] = text2(input[key], `document.policy.${key}`, errors);
3021
3954
  }
3022
3955
  return result;
3023
3956
  }
@@ -3029,7 +3962,7 @@ function themeOf(value, errors) {
3029
3962
  errors.push("document.theme.tokens must be an object");
3030
3963
  return void 0;
3031
3964
  }
3032
- unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
3965
+ unknownKeys2(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
3033
3966
  if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
3034
3967
  let serializedLength = 0;
3035
3968
  for (const [key, token] of Object.entries(tokens)) {
@@ -3049,17 +3982,17 @@ function validateSignupControlDocument(input) {
3049
3982
  const errors = [];
3050
3983
  const root = recordOf(input);
3051
3984
  if (!root) return { ok: false, errors: ["document must be an object"] };
3052
- unknownKeys(root, ROOT_KEYS2, "document", errors);
3985
+ unknownKeys2(root, ROOT_KEYS3, "document", errors);
3053
3986
  if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
3054
3987
  if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
3055
3988
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
3056
3989
  const source = recordOf(root.source);
3057
3990
  const target = recordOf(root.target);
3058
- const appId = text(source?.appId, "document.source.appId", errors, 63);
3059
- const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
3060
- const environment = text(target?.environment, "document.target.environment", errors, 63);
3061
- if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
3062
- if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
3991
+ const appId = text2(source?.appId, "document.source.appId", errors, 63);
3992
+ const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
3993
+ const environment = text2(target?.environment, "document.target.environment", errors, 63);
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");
3063
3996
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
3064
3997
  if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
3065
3998
  const tiers2 = tiersOf(root.tiers, errors);
@@ -3078,6 +4011,8 @@ function validateSignupControlDocument(input) {
3078
4011
  else scheduling = root.scheduling;
3079
4012
  }
3080
4013
  const theme = themeOf(root.theme, errors);
4014
+ const giftOffers = root.giftOffers === void 0 ? void 0 : validateGiftOfferCatalog(root.giftOffers, new Set(tiers2.map((tier) => tier.id)));
4015
+ if (giftOffers && !giftOffers.ok) errors.push(...giftOffers.errors);
3081
4016
  if (errors.length || !page.ok) return { ok: false, errors };
3082
4017
  return { ok: true, value: {
3083
4018
  schemaVersion: 1,
@@ -3090,7 +4025,8 @@ function validateSignupControlDocument(input) {
3090
4025
  ...application ? { application } : {},
3091
4026
  ...policy ? { policy } : {},
3092
4027
  ...scheduling ? { scheduling } : {},
3093
- ...theme ? { theme } : {}
4028
+ ...theme ? { theme } : {},
4029
+ ...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
3094
4030
  } };
3095
4031
  }
3096
4032
 
@@ -3206,7 +4142,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
3206
4142
 
3207
4143
  // src/membership-markdown.ts
3208
4144
  var MAX_BENEFITS2 = 24;
3209
- var MAX_MARKDOWN2 = 65536;
4145
+ var MAX_MARKDOWN3 = 65536;
3210
4146
  var MAX_LABEL2 = 160;
3211
4147
  var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
3212
4148
  var MARKDOWN_HEADING = /^#{1,6}\s+/;
@@ -3231,17 +4167,17 @@ function markdownBenefits(lines, path, errors) {
3231
4167
  errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
3232
4168
  continue;
3233
4169
  }
3234
- const text2 = (match[1] ?? "").trim();
3235
- if (text2.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
3236
- if (text2) benefits.push(text2);
4170
+ const text3 = (match[1] ?? "").trim();
4171
+ if (text3.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
4172
+ if (text3) benefits.push(text3);
3237
4173
  }
3238
4174
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
3239
4175
  return benefits.slice(0, MAX_BENEFITS2);
3240
4176
  }
3241
4177
  function applyMembershipTierMarkdown(presentation, source) {
3242
4178
  if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
3243
- if (source.length > MAX_MARKDOWN2) {
3244
- 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`] };
3245
4181
  }
3246
4182
  if (!source.trim()) return { ok: true, value: { ...presentation } };
3247
4183
  const errors = [];
@@ -3333,7 +4269,7 @@ var exclusions = (chapterId, paidTierIds) => [
3333
4269
  id: chapterId,
3334
4270
  reason: "provider_identity",
3335
4271
  paths: [
3336
- ...paidTierIds.map((id2) => `tiers[${id2}].stripePriceId`),
4272
+ ...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
3337
4273
  "groups.stripePublishableKey",
3338
4274
  "applications.stripeCustomerId",
3339
4275
  "applications.stripeSubscriptionId",
@@ -3375,9 +4311,9 @@ async function exportChapterPromotion(input) {
3375
4311
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
3376
4312
  receiptDigests: [input.digest]
3377
4313
  };
3378
- const artifact = (kind, id2, content) => ({
4314
+ const artifact = (kind, id4, content) => ({
3379
4315
  kind,
3380
- id: id2,
4316
+ id: id4,
3381
4317
  sourceRevision: revision,
3382
4318
  provenance: promotionProvenance,
3383
4319
  content: (0, import_promotion.canonicalPromotionValue)(content)