@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.js CHANGED
@@ -15,6 +15,213 @@ async function applicationIdForSubmission(submissionId) {
15
15
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
16
16
  }
17
17
 
18
+ // src/admission-grants-contract.ts
19
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
20
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
21
+
22
+ // src/signup-control-canonical.ts
23
+ var encoder = new TextEncoder();
24
+ function canonicalValue(value) {
25
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
26
+ return JSON.stringify(value);
27
+ }
28
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
29
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
30
+ const record3 = value;
31
+ const entries = Object.keys(record3).sort().flatMap((key) => {
32
+ const item = record3[key];
33
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
34
+ });
35
+ return `{${entries.join(",")}}`;
36
+ }
37
+ function canonicalSignupControl(value) {
38
+ return canonicalValue(value);
39
+ }
40
+ async function signupControlDigest(document) {
41
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
42
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
43
+ }
44
+ async function createSignupControlEnvelope(document) {
45
+ return { document, digest: await signupControlDigest(document) };
46
+ }
47
+
48
+ // src/admission-grants.ts
49
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
50
+ var MAX_GRANTS = 2e3;
51
+ var MAX_NOTE = 2e3;
52
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
53
+ var isAdmissionEmail = (value) => EMAIL_RE.test(value);
54
+ function normalizeAdmissionEmail(email) {
55
+ return email.trim().toLowerCase();
56
+ }
57
+ function validateAdmissionGrantDocument(input) {
58
+ const errors = [];
59
+ const root = input && typeof input === "object" ? input : {};
60
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
61
+ const revision = Number(root.revision);
62
+ const generatedAt = Number(root.generatedAt);
63
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
64
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
65
+ const source = root.source && typeof root.source === "object" ? root.source : {};
66
+ const target = root.target && typeof root.target === "object" ? root.target : {};
67
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
68
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
69
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
70
+ const environment = typeof target.environment === "string" ? target.environment : "";
71
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
72
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
73
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
74
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
75
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
76
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
77
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
78
+ const grants = [];
79
+ const seenIds = /* @__PURE__ */ new Set();
80
+ const seenEmails = /* @__PURE__ */ new Set();
81
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
82
+ const grant = raw && typeof raw === "object" ? raw : {};
83
+ const id4 = typeof grant.id === "string" ? grant.id : "";
84
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
85
+ const normalized = normalizeAdmissionEmail(email);
86
+ const decision = grant.decision;
87
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
88
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
89
+ if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
90
+ if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
91
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
92
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
93
+ if (decision !== "waive_interview" && decision !== "require_interview") {
94
+ errors.push(`document.grants[${index}].decision is invalid`);
95
+ }
96
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
97
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
98
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
99
+ }
100
+ seenIds.add(id4);
101
+ seenEmails.add(normalized);
102
+ grants.push({
103
+ id: id4,
104
+ email,
105
+ decision,
106
+ ...note ? { note } : {},
107
+ ...expiresAt !== void 0 ? { expiresAt } : {}
108
+ });
109
+ }
110
+ if (errors.length) return { ok: false, errors };
111
+ return {
112
+ ok: true,
113
+ value: {
114
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
115
+ revision,
116
+ generatedAt,
117
+ source: { appId: sourceId, actor: sourceActor },
118
+ target: { chapterId, environment },
119
+ grants
120
+ }
121
+ };
122
+ }
123
+ async function createAdmissionGrantEnvelope(document) {
124
+ const checked = validateAdmissionGrantDocument(document);
125
+ if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
126
+ return { document: checked.value, digest: await signupControlDigest(checked.value) };
127
+ }
128
+ var rows = (value) => Array.isArray(value) ? value : [];
129
+ function asGrant(row) {
130
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
131
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
132
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
133
+ return row;
134
+ }
135
+ async function findAdmissionGrant(db, targetId, email, now) {
136
+ const emailNormalized = normalizeAdmissionEmail(email);
137
+ if (!isAdmissionEmail(emailNormalized)) return null;
138
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
139
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
140
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
141
+ )).sort((a, b) => {
142
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
143
+ return scope || b.createdAt - a.createdAt;
144
+ });
145
+ return active[0] ?? null;
146
+ }
147
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
148
+ const where = { sourceId, targetId, environment };
149
+ if (db.aggregate) {
150
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
151
+ }
152
+ const found = await db.query({ admissionGrantRevisions: {} });
153
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
154
+ }
155
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
156
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
157
+ const result = await db.query({ admissionGrantRevisions: { $: {
158
+ where: { sourceId, targetId, environment },
159
+ order: { revision: "desc" },
160
+ limit: 1
161
+ } } });
162
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
163
+ return row ? row : null;
164
+ }
165
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
166
+ const { document, digest } = envelope;
167
+ const current = rows((await db.query({ admissionGrants: { $: {
168
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
169
+ limit: MAX_GRANTS
170
+ } } })).admissionGrants);
171
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
172
+ const ops = [];
173
+ for (const row of current) {
174
+ const id4 = String(row.id ?? "");
175
+ if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
176
+ active: false,
177
+ revokedAt: receivedAt,
178
+ revokedBy: document.source.actor,
179
+ updatedAt: receivedAt,
180
+ updatedBy: document.source.actor
181
+ } });
182
+ }
183
+ for (const grant of document.grants) {
184
+ const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
185
+ const existing = current.find((row) => row.id === id4);
186
+ ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
187
+ id: id4,
188
+ sourceGrantId: grant.id,
189
+ sourceScope: "parent",
190
+ sourceId: document.source.appId,
191
+ targetId: document.target.chapterId,
192
+ email: grant.email,
193
+ emailNormalized: normalizeAdmissionEmail(grant.email),
194
+ decision: grant.decision,
195
+ ...grant.note ? { note: grant.note } : {},
196
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
197
+ active: true,
198
+ sourceRevision: document.revision,
199
+ createdAt: Number(existing?.createdAt ?? receivedAt),
200
+ createdBy: String(existing?.createdBy ?? document.source.actor),
201
+ updatedAt: receivedAt,
202
+ updatedBy: document.source.actor
203
+ } });
204
+ const retract = [
205
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
206
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
207
+ ];
208
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
209
+ }
210
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
211
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
212
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
213
+ id: revisionId,
214
+ sourceId: document.source.appId,
215
+ targetId: document.target.chapterId,
216
+ environment: document.target.environment,
217
+ revision: document.revision,
218
+ digest,
219
+ active: true,
220
+ receivedAt
221
+ } });
222
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
223
+ }
224
+
18
225
  // src/member.ts
19
226
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
20
227
  var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
@@ -66,9 +273,9 @@ function resolveApplication(a) {
66
273
  validateEmail: a?.validateEmail ?? true
67
274
  };
68
275
  }
69
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
276
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
70
277
  function isValidEmail(value) {
71
- return typeof value === "string" && EMAIL_RE.test(value);
278
+ return typeof value === "string" && EMAIL_RE2.test(value);
72
279
  }
73
280
  function clampArray(value, max) {
74
281
  if (!Array.isArray(value)) return value;
@@ -80,14 +287,14 @@ function hasDisclaimerAck(fields) {
80
287
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
81
288
  function applicantProfile(chapter, fields) {
82
289
  const app = chapter.application;
83
- const allowed = (f) => app.profileFields.includes(f);
290
+ const allowed2 = (f) => app.profileFields.includes(f);
84
291
  const profile = {};
85
292
  for (const f of [...app.required, ...app.optional]) {
86
- if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
293
+ if (IDENTITY_FIELDS.has(f) || !allowed2(f)) continue;
87
294
  const v = fields[f];
88
295
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
89
296
  }
90
- if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
297
+ if (fields.focus !== void 0 && allowed2("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
91
298
  return Object.keys(profile).length > 0 ? profile : void 0;
92
299
  }
93
300
  async function submitApplication(db, chapter, fields, opts) {
@@ -110,14 +317,23 @@ async function submitApplication(db, chapter, fields, opts) {
110
317
  if (app.requireDisclaimerAck && !acked) {
111
318
  return { ok: false, error: "disclaimerAck is required" };
112
319
  }
113
- const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
114
- const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
320
+ const id4 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
321
+ const row = { id: id4, status: chapter.pipeline.initial, createdAt: opts.now };
115
322
  for (const f of [...app.required, ...app.optional]) {
116
323
  if (fieldStates[f]?.visible === false) continue;
117
324
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
118
325
  }
119
326
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
120
327
  if (opts.groupId) row.groupId = opts.groupId;
328
+ if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
329
+ row.emailNormalized = normalizeAdmissionEmail(fields.email);
330
+ }
331
+ if (opts.admissionGrant?.decision === "waive_interview") {
332
+ row.admissionGrantId = opts.admissionGrant.id;
333
+ row.admissionSource = opts.admissionGrant.sourceScope;
334
+ row.interviewWaivedAt = opts.now;
335
+ }
336
+ if (opts.giftOfferId) row.giftOfferId = opts.giftOfferId;
121
337
  if (typeof fields.tierId === "string" && fields.tierId) {
122
338
  if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
123
339
  return { ok: false, error: "tierId is not an offered tier" };
@@ -125,11 +341,20 @@ async function submitApplication(db, chapter, fields, opts) {
125
341
  row.tierId = fields.tierId;
126
342
  }
127
343
  if (acked) row.disclaimerAckAt = opts.now;
344
+ const ops = [
345
+ { t: "update", ns: "applications", id: id4, attrs: row }
346
+ ];
347
+ if (opts.admissionGrant?.decision === "waive_interview") {
348
+ ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
349
+ consumedAt: opts.now,
350
+ consumedApplicationId: id4
351
+ } });
352
+ }
128
353
  const { duplicate } = await db.transact(
129
- [{ t: "update", ns: "applications", id: id2, attrs: row }],
354
+ ops,
130
355
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
131
356
  );
132
- return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
357
+ return { ok: true, id: id4, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
133
358
  }
134
359
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
135
360
  return {
@@ -149,93 +374,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
149
374
  };
150
375
  }
151
376
 
152
- // src/schema.ts
153
- function attr(type, flags = {}) {
154
- return {
155
- type,
156
- unique: flags.unique ?? false,
157
- indexed: flags.indexed ?? false,
158
- optional: flags.optional ?? false
159
- };
160
- }
161
- var id = () => attr("string", { unique: true, indexed: true });
162
- var admins = {
163
- attrs: {
164
- id: id(),
165
- email: attr("string", { unique: true, indexed: true }),
166
- name: attr("string", { optional: true }),
167
- note: attr("string", { optional: true })
168
- }
169
- };
170
- var superAdmins = {
171
- attrs: {
172
- id: id(),
173
- email: attr("string", { unique: true, indexed: true }),
174
- note: attr("string", { optional: true }),
175
- createdAt: attr("number", { indexed: true })
176
- }
177
- };
178
- var networkNotes = {
179
- attrs: {
180
- id: id(),
181
- recordKey: attr("string", { indexed: true }),
182
- targetId: attr("string", { indexed: true }),
183
- recordType: attr("string", { indexed: true }),
184
- recordId: attr("string", { indexed: true }),
185
- body: attr("string"),
186
- authorId: attr("string", { indexed: true }),
187
- authorEmail: attr("string", { optional: true }),
188
- createdAt: attr("number", { indexed: true })
189
- }
190
- };
191
- var signupControls = {
192
- attrs: {
193
- id: id(),
194
- sourceId: attr("string", { indexed: true }),
195
- targetId: attr("string", { indexed: true }),
196
- environment: attr("string", { indexed: true }),
197
- revision: attr("number", { indexed: true }),
198
- digest: attr("string", { indexed: true }),
199
- document: attr("json"),
200
- active: attr("boolean", { indexed: true }),
201
- receivedAt: attr("number", { indexed: true })
202
- }
203
- };
204
- var applications = {
205
- attrs: {
206
- id: id(),
207
- firstName: attr("string"),
208
- lastName: attr("string"),
209
- email: attr("string", { indexed: true }),
210
- referral: attr("string"),
211
- referralName: attr("string", { optional: true }),
212
- whoYouAre: attr("string"),
213
- focus: attr("json"),
214
- linkedin: attr("string", { optional: true }),
215
- message: attr("string"),
216
- status: attr("string", { indexed: true }),
217
- createdAt: attr("number", { indexed: true }),
218
- meetingAt: attr("number", { indexed: true, optional: true }),
219
- meetingLink: attr("string", { optional: true }),
220
- clerkUserId: attr("string", { indexed: true, optional: true }),
221
- // One-time/effect-repair marker. Absent means /api/me should copy the
222
- // allowlisted application snapshot into Clerk private_metadata and remove
223
- // the former public_metadata copy.
224
- clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
225
- phone: attr("string", { optional: true }),
226
- state: attr("string", { optional: true }),
227
- groupId: attr("string", { indexed: true, optional: true }),
228
- // The tier this application joined on. Absent on rows that predate tiers.
229
- tierId: attr("string", { indexed: true, optional: true }),
230
- stripeCustomerId: attr("string", { indexed: true, optional: true }),
231
- stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
232
- renewalAt: attr("number", { optional: true }),
233
- disclaimerAckAt: attr("number", { optional: true }),
234
- refundPolicyAckAt: attr("number", { optional: true }),
235
- prepEmailSentAt: attr("number", { optional: true }),
236
- canceled: attr("boolean", { optional: true })
237
- }
238
- };
377
+ // src/schema-applications.ts
378
+ var attr = (type, flags = {}) => ({
379
+ type,
380
+ unique: flags.unique ?? false,
381
+ indexed: flags.indexed ?? false,
382
+ optional: flags.optional ?? false
383
+ });
239
384
  var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
240
385
  "firstName",
241
386
  "lastName",
@@ -249,25 +394,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
249
394
  "phone",
250
395
  "state"
251
396
  ]);
397
+ var applicationAttrs = {
398
+ id: attr("string", { unique: true, indexed: true }),
399
+ firstName: attr("string"),
400
+ lastName: attr("string"),
401
+ email: attr("string", { indexed: true }),
402
+ referral: attr("string"),
403
+ referralName: attr("string", { optional: true }),
404
+ whoYouAre: attr("string"),
405
+ focus: attr("json"),
406
+ linkedin: attr("string", { optional: true }),
407
+ message: attr("string"),
408
+ status: attr("string", { indexed: true }),
409
+ createdAt: attr("number", { indexed: true }),
410
+ meetingAt: attr("number", { indexed: true, optional: true }),
411
+ meetingLink: attr("string", { optional: true }),
412
+ clerkUserId: attr("string", { indexed: true, optional: true }),
413
+ clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
414
+ phone: attr("string", { optional: true }),
415
+ state: attr("string", { optional: true }),
416
+ groupId: attr("string", { indexed: true, optional: true }),
417
+ tierId: attr("string", { indexed: true, optional: true }),
418
+ stripeCustomerId: attr("string", { indexed: true, optional: true }),
419
+ stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
420
+ renewalAt: attr("number", { optional: true }),
421
+ disclaimerAckAt: attr("number", { optional: true }),
422
+ refundPolicyAckAt: attr("number", { optional: true }),
423
+ prepEmailSentAt: attr("number", { optional: true }),
424
+ canceled: attr("boolean", { optional: true }),
425
+ emailNormalized: attr("string", { indexed: true, optional: true }),
426
+ admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
427
+ admissionSource: attr("string", { indexed: true, optional: true }),
428
+ interviewWaivedAt: attr("number", { optional: true }),
429
+ giftOfferId: attr("string", { optional: true, indexed: true }),
430
+ giftIntentId: attr("string", { optional: true, unique: true, indexed: true }),
431
+ giftClaimId: attr("string", { optional: true, unique: true, indexed: true }),
432
+ giftOrderId: attr("string", { optional: true, indexed: true }),
433
+ giftPaidAt: attr("number", { optional: true, indexed: true }),
434
+ giftRevokedAt: attr("number", { optional: true, indexed: true }),
435
+ giftCheckoutStartedAt: attr("number", { optional: true, indexed: true })
436
+ };
252
437
  function applicationsFor(application) {
253
438
  const attrs = {};
254
- for (const [name, spec] of Object.entries(applications.attrs)) {
255
- attrs[name] = {
256
- ...spec,
257
- optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
258
- };
439
+ for (const [name, spec] of Object.entries(applicationAttrs)) {
440
+ attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
259
441
  }
260
442
  const apply = (field, optional) => {
261
443
  const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
262
- if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
263
- throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
264
- }
265
- if (existing && existing.type !== "string") {
266
- throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
267
- }
268
- const spec = existing ? { ...existing, optional } : attr("string", { optional });
444
+ if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
445
+ if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
269
446
  Object.defineProperty(attrs, field, {
270
- value: spec,
447
+ value: existing ? { ...existing, optional } : attr("string", { optional }),
271
448
  enumerable: true,
272
449
  configurable: true,
273
450
  writable: true
@@ -277,78 +454,297 @@ function applicationsFor(application) {
277
454
  for (const field of application.optional) apply(field, true);
278
455
  return { attrs };
279
456
  }
280
- var groups = {
457
+
458
+ // src/schema-admission.ts
459
+ var attr2 = (type, flags = {}) => ({
460
+ type,
461
+ unique: flags.unique ?? false,
462
+ indexed: flags.indexed ?? false,
463
+ optional: flags.optional ?? false
464
+ });
465
+ var id = () => attr2("string", { unique: true, indexed: true });
466
+ var admissionGrantsEntity = {
281
467
  attrs: {
282
468
  id: id(),
283
- name: attr("string"),
284
- standardPriceCents: attr("number"),
285
- foundingDiscountCents: attr("number"),
286
- stripePriceId: attr("string", { optional: true }),
287
- stripePublishableKey: attr("string", { optional: true }),
288
- notificationEmail: attr("string"),
289
- replyTo: attr("string"),
290
- debugEmail: attr("string", { optional: true }),
291
- calendarLink: attr("string", { optional: true }),
292
- disclaimerText: attr("string"),
293
- refundPolicyText: attr("string"),
294
- trustCopy: attr("string"),
295
- commitmentText: attr("string", { optional: true }),
296
- normsText: attr("string", { optional: true }),
297
- emailTemplates: attr("json"),
298
- schedulingJson: attr("json", { optional: true }),
299
- createdAt: attr("number", { indexed: true })
469
+ sourceGrantId: attr2("string", { indexed: true }),
470
+ sourceScope: attr2("string", { indexed: true }),
471
+ sourceId: attr2("string", { indexed: true }),
472
+ targetId: attr2("string", { indexed: true }),
473
+ email: attr2("string"),
474
+ emailNormalized: attr2("string", { indexed: true }),
475
+ decision: attr2("string", { indexed: true }),
476
+ note: attr2("string", { optional: true }),
477
+ active: attr2("boolean", { indexed: true }),
478
+ sourceRevision: attr2("number", { optional: true, indexed: true }),
479
+ createdAt: attr2("number", { indexed: true }),
480
+ createdBy: attr2("string", { optional: true, indexed: true }),
481
+ updatedAt: attr2("number", { optional: true, indexed: true }),
482
+ updatedBy: attr2("string", { optional: true, indexed: true }),
483
+ expiresAt: attr2("number", { optional: true, indexed: true }),
484
+ revokedAt: attr2("number", { optional: true, indexed: true }),
485
+ revokedBy: attr2("string", { optional: true, indexed: true }),
486
+ consumedAt: attr2("number", { optional: true, indexed: true }),
487
+ consumedApplicationId: attr2("string", { optional: true, indexed: true })
300
488
  }
301
489
  };
302
- var tiers = {
490
+ var admissionGrantRevisionsEntity = {
303
491
  attrs: {
304
492
  id: id(),
305
- groupId: attr("string", { indexed: true }),
306
- name: attr("string"),
307
- priceCents: attr("number"),
308
- stripePriceId: attr("string", { optional: true }),
309
- blurb: attr("string", { optional: true }),
310
- sortOrder: attr("number", { indexed: true }),
311
- active: attr("boolean", { indexed: true }),
312
- createdAt: attr("number", { indexed: true })
493
+ sourceId: attr2("string", { indexed: true }),
494
+ targetId: attr2("string", { indexed: true }),
495
+ environment: attr2("string", { indexed: true }),
496
+ revision: attr2("number", { indexed: true }),
497
+ digest: attr2("string", { indexed: true }),
498
+ active: attr2("boolean", { indexed: true }),
499
+ receivedAt: attr2("number", { indexed: true })
500
+ }
501
+ };
502
+
503
+ // src/schema-gifts.ts
504
+ var attr3 = (type, flags = {}) => ({
505
+ type,
506
+ unique: flags.unique ?? false,
507
+ indexed: flags.indexed ?? false,
508
+ optional: flags.optional ?? false
509
+ });
510
+ var id2 = () => attr3("string", { unique: true, indexed: true });
511
+ var giftPurchaseIntentsEntity = { attrs: {
512
+ id: id2(),
513
+ chapterId: attr3("string", { indexed: true }),
514
+ environment: attr3("string", { indexed: true }),
515
+ purchaserType: attr3("string", { indexed: true }),
516
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
517
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
518
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
519
+ offerId: attr3("string", { indexed: true }),
520
+ offerSnapshot: attr3("json"),
521
+ status: attr3("string", { indexed: true }),
522
+ codeHash: attr3("string", { indexed: true }),
523
+ codeVersion: attr3("number"),
524
+ codeIssuedAt: attr3("number"),
525
+ providerObjectId: attr3("string", { optional: true, unique: true, indexed: true }),
526
+ providerCustomerId: attr3("string", { optional: true, indexed: true }),
527
+ checkoutStartedAt: attr3("number", { optional: true }),
528
+ paidAt: attr3("number", { optional: true, indexed: true }),
529
+ claimId: attr3("string", { optional: true, unique: true, indexed: true }),
530
+ createdAt: attr3("number", { indexed: true }),
531
+ updatedAt: attr3("number", { indexed: true })
532
+ } };
533
+ var giftOrdersEntity = { attrs: {
534
+ id: id2(),
535
+ intentId: attr3("string", { unique: true, indexed: true }),
536
+ chapterId: attr3("string", { indexed: true }),
537
+ environment: attr3("string", { indexed: true }),
538
+ offerId: attr3("string", { indexed: true }),
539
+ offerSnapshot: attr3("json"),
540
+ purchaserType: attr3("string", { indexed: true }),
541
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
542
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
543
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
544
+ providerEventId: attr3("string", { unique: true, indexed: true }),
545
+ providerObjectId: attr3("string", { indexed: true }),
546
+ amountCents: attr3("number"),
547
+ currency: attr3("string"),
548
+ billingMode: attr3("string"),
549
+ createdAt: attr3("number", { indexed: true })
550
+ } };
551
+ var giftClaimsEntity = { attrs: {
552
+ id: id2(),
553
+ orderId: attr3("string", { unique: true, indexed: true }),
554
+ intentId: attr3("string", { unique: true, indexed: true }),
555
+ chapterId: attr3("string", { indexed: true }),
556
+ offerId: attr3("string", { indexed: true }),
557
+ target: attr3("json"),
558
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
559
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
560
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
561
+ codeHash: attr3("string", { unique: true, indexed: true }),
562
+ codeVersion: attr3("number"),
563
+ rotationCount: attr3("number"),
564
+ codeIssuedAt: attr3("number"),
565
+ codeExpiresAt: attr3("number", { indexed: true }),
566
+ attemptCount: attr3("number"),
567
+ attemptWindowStartedAt: attr3("number"),
568
+ status: attr3("string", { indexed: true }),
569
+ redeemedAt: attr3("number", { optional: true, indexed: true }),
570
+ beneficiaryEmailNormalized: attr3("string", { optional: true, indexed: true }),
571
+ beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
572
+ applicationId: attr3("string", { optional: true, unique: true, indexed: true }),
573
+ entitlementId: attr3("string", { optional: true, unique: true, indexed: true }),
574
+ rotatedAt: attr3("number", { optional: true }),
575
+ revokedAt: attr3("number", { optional: true, indexed: true }),
576
+ revokedReason: attr3("string", { optional: true }),
577
+ createdAt: attr3("number", { indexed: true })
578
+ } };
579
+ var giftEntitlementsEntity = { attrs: {
580
+ id: id2(),
581
+ claimId: attr3("string", { unique: true, indexed: true }),
582
+ orderId: attr3("string", { indexed: true }),
583
+ chapterId: attr3("string", { indexed: true }),
584
+ entitlementKey: attr3("string", { indexed: true }),
585
+ beneficiaryEmailNormalized: attr3("string", { indexed: true }),
586
+ beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
587
+ active: attr3("boolean", { indexed: true }),
588
+ createdAt: attr3("number", { indexed: true }),
589
+ revokedAt: attr3("number", { optional: true })
590
+ } };
591
+ var giftRedemptionAttemptsEntity = { attrs: {
592
+ id: id2(),
593
+ chapterId: attr3("string", { indexed: true }),
594
+ bucket: attr3("string", { indexed: true }),
595
+ attemptCount: attr3("number"),
596
+ windowStartedAt: attr3("number", { indexed: true }),
597
+ updatedAt: attr3("number")
598
+ } };
599
+
600
+ // src/schema.ts
601
+ function attr4(type, flags = {}) {
602
+ return {
603
+ type,
604
+ unique: flags.unique ?? false,
605
+ indexed: flags.indexed ?? false,
606
+ optional: flags.optional ?? false
607
+ };
608
+ }
609
+ var id3 = () => attr4("string", { unique: true, indexed: true });
610
+ var admins = {
611
+ attrs: {
612
+ id: id3(),
613
+ email: attr4("string", { unique: true, indexed: true }),
614
+ name: attr4("string", { optional: true }),
615
+ note: attr4("string", { optional: true })
616
+ }
617
+ };
618
+ var superAdmins = {
619
+ attrs: {
620
+ id: id3(),
621
+ email: attr4("string", { unique: true, indexed: true }),
622
+ note: attr4("string", { optional: true }),
623
+ createdAt: attr4("number", { indexed: true })
624
+ }
625
+ };
626
+ var networkNotes = {
627
+ attrs: {
628
+ id: id3(),
629
+ recordKey: attr4("string", { indexed: true }),
630
+ targetId: attr4("string", { indexed: true }),
631
+ recordType: attr4("string", { indexed: true }),
632
+ recordId: attr4("string", { indexed: true }),
633
+ body: attr4("string"),
634
+ authorId: attr4("string", { indexed: true }),
635
+ authorEmail: attr4("string", { optional: true }),
636
+ createdAt: attr4("number", { indexed: true })
637
+ }
638
+ };
639
+ var signupControls = {
640
+ attrs: {
641
+ id: id3(),
642
+ sourceId: attr4("string", { indexed: true }),
643
+ targetId: attr4("string", { indexed: true }),
644
+ environment: attr4("string", { indexed: true }),
645
+ revision: attr4("number", { indexed: true }),
646
+ digest: attr4("string", { indexed: true }),
647
+ document: attr4("json"),
648
+ active: attr4("boolean", { indexed: true }),
649
+ receivedAt: attr4("number", { indexed: true })
650
+ }
651
+ };
652
+ var memberContentControls = {
653
+ attrs: {
654
+ id: id3(),
655
+ sourceId: attr4("string", { indexed: true }),
656
+ targetId: attr4("string", { indexed: true }),
657
+ environment: attr4("string", { indexed: true }),
658
+ revision: attr4("number", { indexed: true }),
659
+ digest: attr4("string", { indexed: true }),
660
+ document: attr4("json"),
661
+ active: attr4("boolean", { indexed: true }),
662
+ receivedAt: attr4("number", { indexed: true })
663
+ }
664
+ };
665
+ var memberContentLocal = {
666
+ attrs: {
667
+ id: id3(),
668
+ chapterId: attr4("string", { indexed: true }),
669
+ environment: attr4("string", { indexed: true }),
670
+ revision: attr4("number", { indexed: true }),
671
+ document: attr4("json"),
672
+ updatedAt: attr4("number", { indexed: true }),
673
+ updatedBy: attr4("string", { optional: true })
674
+ }
675
+ };
676
+ var groups = {
677
+ attrs: {
678
+ id: id3(),
679
+ name: attr4("string"),
680
+ standardPriceCents: attr4("number"),
681
+ foundingDiscountCents: attr4("number"),
682
+ stripePriceId: attr4("string", { optional: true }),
683
+ stripePublishableKey: attr4("string", { optional: true }),
684
+ notificationEmail: attr4("string"),
685
+ replyTo: attr4("string"),
686
+ debugEmail: attr4("string", { optional: true }),
687
+ calendarLink: attr4("string", { optional: true }),
688
+ disclaimerText: attr4("string"),
689
+ refundPolicyText: attr4("string"),
690
+ trustCopy: attr4("string"),
691
+ commitmentText: attr4("string", { optional: true }),
692
+ normsText: attr4("string", { optional: true }),
693
+ emailTemplates: attr4("json"),
694
+ schedulingJson: attr4("json", { optional: true }),
695
+ createdAt: attr4("number", { indexed: true })
696
+ }
697
+ };
698
+ var tiers = {
699
+ attrs: {
700
+ id: id3(),
701
+ groupId: attr4("string", { indexed: true }),
702
+ name: attr4("string"),
703
+ priceCents: attr4("number"),
704
+ stripePriceId: attr4("string", { optional: true }),
705
+ blurb: attr4("string", { optional: true }),
706
+ sortOrder: attr4("number", { indexed: true }),
707
+ active: attr4("boolean", { indexed: true }),
708
+ createdAt: attr4("number", { indexed: true })
313
709
  }
314
710
  };
315
711
  var meetings = {
316
712
  attrs: {
317
- id: id(),
318
- applicationId: attr("string", { indexed: true }),
319
- groupId: attr("string", { indexed: true }),
320
- startAt: attr("number", { indexed: true }),
321
- endAt: attr("number"),
322
- timezone: attr("string"),
323
- status: attr("string", { indexed: true }),
324
- googleEventId: attr("string", { indexed: true, optional: true }),
325
- meetUrl: attr("string", { optional: true }),
326
- htmlLink: attr("string", { optional: true }),
327
- drift: attr("string", { indexed: true, optional: true }),
328
- driftGoogleStartAt: attr("number", { optional: true }),
329
- driftDetectedAt: attr("number", { optional: true }),
330
- adoptedFromGoogleAt: attr("number", { optional: true }),
331
- createdAt: attr("number", { indexed: true })
713
+ id: id3(),
714
+ applicationId: attr4("string", { indexed: true }),
715
+ groupId: attr4("string", { indexed: true }),
716
+ startAt: attr4("number", { indexed: true }),
717
+ endAt: attr4("number"),
718
+ timezone: attr4("string"),
719
+ status: attr4("string", { indexed: true }),
720
+ googleEventId: attr4("string", { indexed: true, optional: true }),
721
+ meetUrl: attr4("string", { optional: true }),
722
+ htmlLink: attr4("string", { optional: true }),
723
+ drift: attr4("string", { indexed: true, optional: true }),
724
+ driftGoogleStartAt: attr4("number", { optional: true }),
725
+ driftDetectedAt: attr4("number", { optional: true }),
726
+ adoptedFromGoogleAt: attr4("number", { optional: true }),
727
+ createdAt: attr4("number", { indexed: true })
332
728
  }
333
729
  };
334
730
  var emailLog = {
335
731
  attrs: {
336
- id: id(),
337
- groupId: attr("string", { indexed: true }),
338
- applicationId: attr("string", { indexed: true, optional: true }),
339
- to: attr("string", { indexed: true }),
340
- template: attr("string", { indexed: true }),
341
- subject: attr("string"),
342
- body: attr("string", { optional: true }),
343
- transport: attr("string"),
344
- messageId: attr("string", { optional: true }),
345
- redirected: attr("boolean", { optional: true }),
346
- dedupeKey: attr("string", { indexed: true, optional: true }),
347
- error: attr("string", { optional: true }),
348
- sentAt: attr("number", { indexed: true })
732
+ id: id3(),
733
+ groupId: attr4("string", { indexed: true }),
734
+ applicationId: attr4("string", { indexed: true, optional: true }),
735
+ to: attr4("string", { indexed: true }),
736
+ template: attr4("string", { indexed: true }),
737
+ subject: attr4("string"),
738
+ body: attr4("string", { optional: true }),
739
+ transport: attr4("string"),
740
+ messageId: attr4("string", { optional: true }),
741
+ redirected: attr4("boolean", { optional: true }),
742
+ dedupeKey: attr4("string", { indexed: true, optional: true }),
743
+ error: attr4("string", { optional: true }),
744
+ sentAt: attr4("number", { indexed: true })
349
745
  }
350
746
  };
351
- function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
747
+ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false, includeMemberContent = false) {
352
748
  const entities = {};
353
749
  if (mode === "chapter") {
354
750
  entities.applications = applicationsFor(application);
@@ -356,7 +752,18 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
356
752
  entities.tiers = tiers;
357
753
  entities.meetings = meetings;
358
754
  entities.emailLog = emailLog;
755
+ entities.admissionGrants = admissionGrantsEntity;
756
+ entities.admissionGrantRevisions = admissionGrantRevisionsEntity;
757
+ entities.giftPurchaseIntents = giftPurchaseIntentsEntity;
758
+ entities.giftOrders = giftOrdersEntity;
759
+ entities.giftClaims = giftClaimsEntity;
760
+ entities.giftEntitlements = giftEntitlementsEntity;
761
+ entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
359
762
  if (includeSignupControl) entities.signupControls = signupControls;
763
+ if (includeMemberContent) {
764
+ entities.memberContentControls = memberContentControls;
765
+ entities.memberContentLocal = memberContentLocal;
766
+ }
360
767
  }
361
768
  if (auth.source === "table") entities.admins = admins;
362
769
  if (auth.superAdmins) entities.superAdmins = superAdmins;
@@ -640,6 +1047,7 @@ var DEFAULT_ADMIN_COPY = {
640
1047
  overview: "Overview",
641
1048
  billing: "Billing",
642
1049
  people: "People",
1050
+ admission: "Admission",
643
1051
  network: "Network",
644
1052
  portfolio: "Portfolio",
645
1053
  settings: "Settings",
@@ -750,6 +1158,26 @@ var DEFAULT_ADMIN_COPY = {
750
1158
  reschedule: "Reschedule",
751
1159
  cancel: "Cancel"
752
1160
  },
1161
+ admission: {
1162
+ title: "Interview decisions",
1163
+ description: "Local decisions override Parent decisions. Requiring an interview locally suppresses a Parent waiver for the same email.",
1164
+ email: "Email",
1165
+ decision: "Decision",
1166
+ expires: "Expires",
1167
+ note: "Internal note",
1168
+ waive: "Waive interview",
1169
+ require: "Require interview",
1170
+ add: "Add decision",
1171
+ ledger: "Effective grant ledger",
1172
+ loading: "Loading\u2026",
1173
+ consumed: "consumed",
1174
+ revoked: "revoked",
1175
+ expired: "expired",
1176
+ active: "active",
1177
+ local: "local",
1178
+ parent: "Parent",
1179
+ revoke: "Revoke"
1180
+ },
753
1181
  network: {
754
1182
  title: "Share to a follower",
755
1183
  allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
@@ -848,6 +1276,19 @@ var DEFAULT_JOIN_COPY = {
848
1276
  label: "You're booked",
849
1277
  calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
850
1278
  memberArea: "Go to your member area"
1279
+ },
1280
+ accepted: {
1281
+ label: "You're approved",
1282
+ body: "Your membership is approved. No introduction call is required."
1283
+ },
1284
+ gift: {
1285
+ chooseSelf: "Buy membership for myself",
1286
+ optionsLabel: "Or give access to someone else",
1287
+ label: "Your gift code",
1288
+ body: "Share this one-time code with the recipient. It becomes active after payment is confirmed.",
1289
+ save: "Save it now. For security, this code is shown only once.",
1290
+ rotate: "Generate a new code",
1291
+ rotateFailed: "A new code could not be generated."
851
1292
  }
852
1293
  };
853
1294
  var DEFAULT_MEMBERS_COPY = {
@@ -875,6 +1316,26 @@ var DEFAULT_MEMBERS_COPY = {
875
1316
  full: {
876
1317
  welcome: "Welcome back."
877
1318
  },
1319
+ gifts: {
1320
+ title: "Give access",
1321
+ body: "Purchase access for someone else and share the one-time claim code.",
1322
+ noneAvailable: "No gift options are available right now.",
1323
+ paid: "Payment submitted. Save the code below while it is visible.",
1324
+ saveCode: "This code is shown once. Save it before leaving this page.",
1325
+ purchases: "Your gifts",
1326
+ noPurchases: "No gift purchases yet.",
1327
+ pending: "Pending",
1328
+ redeemed: "Redeemed",
1329
+ expired: "Expired",
1330
+ revoked: "Revoked",
1331
+ rotate: "Rotate code",
1332
+ rotated: "A new code was generated. Save it now.",
1333
+ rotateFailed: "The code could not be rotated.",
1334
+ redeemTitle: "Redeem a gift",
1335
+ code: "One-time code",
1336
+ redeem: "Redeem",
1337
+ redeemFailed: "The code is invalid or unavailable."
1338
+ },
878
1339
  reschedule: {
879
1340
  changeTime: "Change your time",
880
1341
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
@@ -990,13 +1451,13 @@ function resolveLeaderFormation(input, crm) {
990
1451
  }
991
1452
  function formationFields(crm, formation) {
992
1453
  const def = crm.type(formation.type);
993
- return [...formation.required, ...formation.optional].map((id2) => {
994
- const field = def.fields[id2];
1454
+ return [...formation.required, ...formation.optional].map((id4) => {
1455
+ const field = def.fields[id4];
995
1456
  return {
996
- id: id2,
997
- label: field.label ?? id2,
1457
+ id: id4,
1458
+ label: field.label ?? id4,
998
1459
  type: field.type,
999
- required: formation.required.includes(id2),
1460
+ required: formation.required.includes(id4),
1000
1461
  ...field.options ? { options: field.options } : {}
1001
1462
  };
1002
1463
  });
@@ -1148,16 +1609,36 @@ function resolveSignupControl(input) {
1148
1609
  return { sourceId: input.sourceId, secretName, stripeMode: input.stripeMode };
1149
1610
  }
1150
1611
 
1151
- // src/config.ts
1612
+ // src/config-member-content.ts
1152
1613
  var SLUG3 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1614
+ var SECRET_NAME2 = /^[a-z][a-z0-9_]{1,127}$/;
1615
+ function resolveMemberContent(input) {
1616
+ if (!input) return null;
1617
+ const parent = input.parent;
1618
+ if (parent && !SLUG3.test(parent.sourceId)) {
1619
+ throw new Error("defineChapter.memberContent.parent.sourceId: must be a lowercase app slug");
1620
+ }
1621
+ const secretName = parent?.secretName?.trim() || "member_content_control_secret";
1622
+ if (parent && !SECRET_NAME2.test(secretName)) {
1623
+ throw new Error("defineChapter.memberContent.parent.secretName: must be a vault-key identifier");
1624
+ }
1625
+ return {
1626
+ enabled: true,
1627
+ localAuthoring: input.localAuthoring !== false,
1628
+ parent: parent ? { sourceId: parent.sourceId, secretName } : null
1629
+ };
1630
+ }
1631
+
1632
+ // src/config.ts
1633
+ var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
1153
1634
  function isResolvedCrm(x) {
1154
1635
  return !!x && typeof x === "object" && "prepare" in x && typeof x.prepare === "function";
1155
1636
  }
1156
1637
  function defineChapter(config) {
1157
1638
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
1158
- const { id: id2, name } = config;
1159
- if (typeof id2 !== "string" || !SLUG3.test(id2)) {
1160
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
1639
+ const { id: id4, name } = config;
1640
+ if (typeof id4 !== "string" || !SLUG4.test(id4)) {
1641
+ throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
1161
1642
  }
1162
1643
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
1163
1644
  const mode = config.mode ?? "chapter";
@@ -1181,16 +1662,21 @@ function defineChapter(config) {
1181
1662
  const copy = resolveChapterCopy(config.copy);
1182
1663
  const network = resolveNetwork(config, crm);
1183
1664
  const signupControl = resolveSignupControl(config.signupControl);
1665
+ const memberContent = resolveMemberContent(config.memberContent);
1184
1666
  const formation = resolveLeaderFormation(config.formation, crm);
1185
1667
  if (signupControl && mode !== "chapter") {
1186
1668
  throw new Error("defineChapter.signupControl: only chapter-mode followers can receive signup control");
1187
1669
  }
1670
+ if (memberContent && mode !== "chapter") {
1671
+ throw new Error("defineChapter.memberContent: only chapter-mode sites can serve member content");
1672
+ }
1188
1673
  const { schema, rules } = chapterDb(
1189
1674
  mode,
1190
1675
  auth,
1191
1676
  network.targets.length > 0,
1192
1677
  application,
1193
- Boolean(signupControl)
1678
+ Boolean(signupControl),
1679
+ Boolean(memberContent)
1194
1680
  );
1195
1681
  const services = config.services ?? ["db", "calendar", "o11y"];
1196
1682
  const account = config.account ?? "none";
@@ -1240,13 +1726,14 @@ function defineChapter(config) {
1240
1726
  };
1241
1727
  const chapter = {
1242
1728
  config,
1243
- id: id2,
1729
+ id: id4,
1244
1730
  name,
1245
1731
  mode,
1246
1732
  brand,
1247
1733
  copy,
1248
1734
  network,
1249
1735
  signupControl,
1736
+ memberContent,
1250
1737
  formation,
1251
1738
  crm,
1252
1739
  auth,
@@ -1454,8 +1941,8 @@ function tierPayable(tier, group, hasSecretKey) {
1454
1941
  function byOrder(a, b) {
1455
1942
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1456
1943
  }
1457
- function resolveTiers(group, rows = []) {
1458
- const stored = rows.filter((tier) => tier.active);
1944
+ function resolveTiers(group, rows2 = []) {
1945
+ const stored = rows2.filter((tier) => tier.active);
1459
1946
  if (stored.length) return [...stored].sort(byOrder);
1460
1947
  return [{
1461
1948
  id: "standard",
@@ -1541,12 +2028,332 @@ function createChapterIntegration(chapter, options = {}) {
1541
2028
  }] : [],
1542
2029
  // Formation metadata is public only when the host explicitly enables
1543
2030
  // the bounded intake contract.
1544
- ...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : []
2031
+ ...chapter.formation.enabled ? [{ path: "/api/formation/config", expectedStatus: 200 }] : [],
2032
+ ...chapter.memberContent ? [
2033
+ { path: "/api/member-content", expectedStatus: 401 },
2034
+ { path: "/api/admin/member-content", expectedStatus: 401 }
2035
+ ] : []
1545
2036
  ],
1546
2037
  runbooks
1547
2038
  };
1548
2039
  }
1549
2040
 
2041
+ // src/member-content-validation-helpers.ts
2042
+ var ID2 = /^[a-z][a-z0-9-]{0,79}$/;
2043
+ var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2044
+ var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2045
+ var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
2046
+ var TARGET_KEYS = /* @__PURE__ */ new Set(["chapterId", "environment"]);
2047
+ var PAGE_KEYS = /* @__PURE__ */ new Set(["id", "slug", "title", "summary", "navigation", "audience", "teaserMarkdown", "bodyMarkdown", "upsell"]);
2048
+ var NAV_KEYS = /* @__PURE__ */ new Set(["section", "order", "parentId"]);
2049
+ var AUDIENCE_KEYS = /* @__PURE__ */ new Set(["kind", "tierIds"]);
2050
+ var UPSELL_KEYS = /* @__PURE__ */ new Set(["owner", "offer"]);
2051
+ var OFFER_KEYS = /* @__PURE__ */ new Set(["tierId", "label", "bodyMarkdown", "ctaLabel"]);
2052
+ var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["pageId", "offer"]);
2053
+ var MAX_PAGES = 100;
2054
+ var MAX_MARKDOWN = 65536;
2055
+ var MAX_TEXT = 240;
2056
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2057
+ function unknownKeys(value, allowed2, path, errors) {
2058
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2059
+ }
2060
+ function textValue(value, path, errors, max = MAX_TEXT, required = false) {
2061
+ if (value === void 0) {
2062
+ if (required) errors.push(`${path} is required`);
2063
+ return void 0;
2064
+ }
2065
+ if (typeof value !== "string") {
2066
+ errors.push(`${path} must be a string`);
2067
+ return void 0;
2068
+ }
2069
+ const valueText = value.trim();
2070
+ if (required && !valueText) errors.push(`${path} is required`);
2071
+ if (valueText.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2072
+ return valueText;
2073
+ }
2074
+ function offerValue(value, path, errors, tiers2, audience) {
2075
+ const raw = record(value);
2076
+ if (!raw) {
2077
+ errors.push(`${path} must be an object`);
2078
+ return void 0;
2079
+ }
2080
+ unknownKeys(raw, OFFER_KEYS, path, errors);
2081
+ const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2082
+ if (tierId && !ID2.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2083
+ if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2084
+ if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2085
+ errors.push(`${path}.tierId must grant access to this page`);
2086
+ }
2087
+ const label = textValue(raw.label, `${path}.label`, errors);
2088
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN);
2089
+ const ctaLabel = textValue(raw.ctaLabel, `${path}.ctaLabel`, errors);
2090
+ return {
2091
+ tierId,
2092
+ ...label ? { label } : {},
2093
+ ...bodyMarkdown ? { bodyMarkdown } : {},
2094
+ ...ctaLabel ? { ctaLabel } : {}
2095
+ };
2096
+ }
2097
+
2098
+ // src/member-content-validation.ts
2099
+ function validateMemberContentDocument(input, options = {}) {
2100
+ const errors = [];
2101
+ const root = record(input);
2102
+ if (!root) return { ok: false, errors: ["document must be an object"] };
2103
+ unknownKeys(root, ROOT_KEYS, "document", errors);
2104
+ if (root.schemaVersion !== MEMBER_CONTENT_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
2105
+ if (!Number.isInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
2106
+ if (!Number.isInteger(root.generatedAt) || Number(root.generatedAt) < 0) errors.push("document.generatedAt must be a non-negative integer timestamp");
2107
+ const sourceRaw = record(root.source);
2108
+ let source = { type: "chapter", appId: "invalid" };
2109
+ if (!sourceRaw) errors.push("document.source must be an object");
2110
+ else {
2111
+ unknownKeys(sourceRaw, SOURCE_KEYS, "document.source", errors);
2112
+ const type = sourceRaw.type;
2113
+ const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
2114
+ if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
2115
+ if (appId && !ID2.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
2116
+ if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2117
+ if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
2118
+ if (type === "parent" || type === "chapter") source = { type, appId };
2119
+ }
2120
+ let target;
2121
+ if (root.target !== void 0) {
2122
+ const targetRaw = record(root.target);
2123
+ if (!targetRaw) errors.push("document.target must be an object");
2124
+ else {
2125
+ unknownKeys(targetRaw, TARGET_KEYS, "document.target", errors);
2126
+ const chapterId = textValue(targetRaw.chapterId, "document.target.chapterId", errors, 63, true) ?? "";
2127
+ const environment = textValue(targetRaw.environment, "document.target.environment", errors, 80, true) ?? "";
2128
+ target = { chapterId, environment };
2129
+ }
2130
+ }
2131
+ if (source.type === "parent" && !target) errors.push("document.target is required for parent content");
2132
+ if (source.type === "chapter" && target) errors.push("document.target is not supported for chapter content");
2133
+ if (!Array.isArray(root.pages)) errors.push("document.pages must be an array");
2134
+ const rawPages = Array.isArray(root.pages) ? root.pages : [];
2135
+ if (rawPages.length > MAX_PAGES) errors.push(`document.pages supports at most ${MAX_PAGES} entries`);
2136
+ const tierIds = new Set((options.tiers ?? []).map((tier) => tier.id));
2137
+ const knownParentIds = new Set((options.parentPages ?? []).map((page) => page.id));
2138
+ const ids = /* @__PURE__ */ new Set();
2139
+ const slugs = /* @__PURE__ */ new Set();
2140
+ const pages = [];
2141
+ for (const [index, value] of rawPages.slice(0, MAX_PAGES).entries()) {
2142
+ const path = `document.pages[${index}]`;
2143
+ const raw = record(value);
2144
+ if (!raw) {
2145
+ errors.push(`${path} must be an object`);
2146
+ continue;
2147
+ }
2148
+ unknownKeys(raw, PAGE_KEYS, path, errors);
2149
+ const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
2150
+ const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
2151
+ const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2152
+ if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2153
+ if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2154
+ if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
2155
+ if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
2156
+ ids.add(id4);
2157
+ slugs.add(slug);
2158
+ const navRaw = record(raw.navigation);
2159
+ let navigation = { section: "General", order: index };
2160
+ if (!navRaw) errors.push(`${path}.navigation must be an object`);
2161
+ else {
2162
+ unknownKeys(navRaw, NAV_KEYS, `${path}.navigation`, errors);
2163
+ const section = textValue(navRaw.section, `${path}.navigation.section`, errors, 120, true) ?? "";
2164
+ const order = navRaw.order;
2165
+ if (!Number.isInteger(order) || Number(order) < 0 || Number(order) > 1e6) {
2166
+ errors.push(`${path}.navigation.order must be an integer from 0 to 1,000,000`);
2167
+ }
2168
+ const parentId = textValue(navRaw.parentId, `${path}.navigation.parentId`, errors, 80);
2169
+ navigation = { section, order: Number.isInteger(order) ? Number(order) : index, ...parentId ? { parentId } : {} };
2170
+ }
2171
+ const audienceRaw = record(raw.audience);
2172
+ let audience = { kind: "all-members" };
2173
+ if (!audienceRaw) errors.push(`${path}.audience must be an object`);
2174
+ else {
2175
+ unknownKeys(audienceRaw, AUDIENCE_KEYS, `${path}.audience`, errors);
2176
+ if (audienceRaw.kind === "all-members") {
2177
+ if (audienceRaw.tierIds !== void 0) errors.push(`${path}.audience.tierIds is not supported for all-members`);
2178
+ } else if (audienceRaw.kind === "tiers") {
2179
+ if (!Array.isArray(audienceRaw.tierIds) || audienceRaw.tierIds.length === 0) {
2180
+ errors.push(`${path}.audience.tierIds must be a non-empty array`);
2181
+ }
2182
+ const values = Array.isArray(audienceRaw.tierIds) ? audienceRaw.tierIds : [];
2183
+ const normalized = [];
2184
+ for (const [tierIndex, value2] of values.entries()) {
2185
+ const tier = textValue(value2, `${path}.audience.tierIds[${tierIndex}]`, errors, 80, true) ?? "";
2186
+ if (normalized.includes(tier)) errors.push(`${path}.audience.tierIds duplicates "${tier}"`);
2187
+ if (tierIds.size && !tierIds.has(tier)) errors.push(`${path}.audience.tierIds references undeclared tier "${tier}"`);
2188
+ normalized.push(tier);
2189
+ }
2190
+ audience = { kind: "tiers", tierIds: normalized };
2191
+ } else errors.push(`${path}.audience.kind must be all-members or tiers`);
2192
+ }
2193
+ const summary = textValue(raw.summary, `${path}.summary`, errors, 2e3);
2194
+ const teaserMarkdown = textValue(raw.teaserMarkdown, `${path}.teaserMarkdown`, errors, MAX_MARKDOWN);
2195
+ const bodyMarkdown = textValue(raw.bodyMarkdown, `${path}.bodyMarkdown`, errors, MAX_MARKDOWN, true) ?? "";
2196
+ let upsell;
2197
+ if (raw.upsell !== void 0) {
2198
+ const upsellRaw = record(raw.upsell);
2199
+ if (!upsellRaw) errors.push(`${path}.upsell must be an object`);
2200
+ else {
2201
+ unknownKeys(upsellRaw, UPSELL_KEYS, `${path}.upsell`, errors);
2202
+ const owner = upsellRaw.owner;
2203
+ if (owner !== "parent" && owner !== "chapter") errors.push(`${path}.upsell.owner must be parent or chapter`);
2204
+ if (source.type === "chapter" && owner === "parent") errors.push(`${path}.upsell.owner cannot be parent on a chapter page`);
2205
+ if (source.type === "parent" && owner === "parent" && upsellRaw.offer === void 0) errors.push(`${path}.upsell.offer is required when the parent owns the offer`);
2206
+ if (source.type === "parent" && owner === "chapter" && upsellRaw.offer !== void 0) errors.push(`${path}.upsell.offer must be set by the chapter after delegation`);
2207
+ const offer = upsellRaw.offer === void 0 ? void 0 : offerValue(upsellRaw.offer, `${path}.upsell.offer`, errors, tierIds, audience);
2208
+ if (owner === "parent") upsell = { owner, offer: offer ?? { tierId: "" } };
2209
+ else if (owner === "chapter") upsell = { owner, ...offer ? { offer } : {} };
2210
+ }
2211
+ }
2212
+ pages.push({ id: id4, slug, title, ...summary ? { summary } : {}, navigation, audience, ...teaserMarkdown ? { teaserMarkdown } : {}, bodyMarkdown, ...upsell ? { upsell } : {} });
2213
+ }
2214
+ const allIds = /* @__PURE__ */ new Set([...knownParentIds, ...ids]);
2215
+ for (const [index, page] of pages.entries()) {
2216
+ const parentId = page.navigation.parentId;
2217
+ if (parentId && !allIds.has(parentId)) errors.push(`document.pages[${index}].navigation.parentId references unknown page "${parentId}"`);
2218
+ if (parentId === page.id) errors.push(`document.pages[${index}].navigation.parentId cannot reference itself`);
2219
+ }
2220
+ for (const page of pages) {
2221
+ const seen = /* @__PURE__ */ new Set([page.id]);
2222
+ let parentId = page.navigation.parentId;
2223
+ while (parentId) {
2224
+ if (seen.has(parentId)) {
2225
+ errors.push(`document.pages contains a navigation cycle at "${page.id}"`);
2226
+ break;
2227
+ }
2228
+ seen.add(parentId);
2229
+ parentId = pages.find((candidate) => candidate.id === parentId)?.navigation.parentId;
2230
+ }
2231
+ }
2232
+ let upsellOverrides;
2233
+ if (root.upsellOverrides !== void 0) {
2234
+ if (source.type !== "chapter") errors.push("document.upsellOverrides is supported only for chapter content");
2235
+ if (!Array.isArray(root.upsellOverrides)) errors.push("document.upsellOverrides must be an array");
2236
+ const values = Array.isArray(root.upsellOverrides) ? root.upsellOverrides : [];
2237
+ const seen = /* @__PURE__ */ new Set();
2238
+ upsellOverrides = [];
2239
+ for (const [index, value] of values.entries()) {
2240
+ const path = `document.upsellOverrides[${index}]`;
2241
+ const raw = record(value);
2242
+ if (!raw) {
2243
+ errors.push(`${path} must be an object`);
2244
+ continue;
2245
+ }
2246
+ unknownKeys(raw, OVERRIDE_KEYS, path, errors);
2247
+ const pageId = textValue(raw.pageId, `${path}.pageId`, errors, 80, true) ?? "";
2248
+ if (seen.has(pageId)) errors.push(`${path}.pageId duplicates "${pageId}"`);
2249
+ seen.add(pageId);
2250
+ const parentPage = (options.parentPages ?? []).find((page) => page.id === pageId);
2251
+ if (!parentPage) errors.push(`${path}.pageId must reference a parent page`);
2252
+ else if (parentPage.upsell?.owner !== "chapter") errors.push(`${path}.pageId does not delegate upsell authority to the chapter`);
2253
+ const offer = offerValue(raw.offer, `${path}.offer`, errors, tierIds, parentPage?.audience);
2254
+ if (offer) upsellOverrides.push({ pageId, offer });
2255
+ }
2256
+ }
2257
+ if (errors.length) return { ok: false, errors };
2258
+ return { ok: true, value: {
2259
+ schemaVersion: MEMBER_CONTENT_SCHEMA_VERSION,
2260
+ revision: Number(root.revision),
2261
+ generatedAt: Number(root.generatedAt),
2262
+ source,
2263
+ ...target ? { target } : {},
2264
+ pages,
2265
+ ...upsellOverrides ? { upsellOverrides } : {}
2266
+ } };
2267
+ }
2268
+ function memberContentDocument(input, options = {}) {
2269
+ const result = validateMemberContentDocument(input, options);
2270
+ if (!result.ok) throw new TypeError(`Invalid member content document: ${result.errors.join("; ")}`);
2271
+ return result.value;
2272
+ }
2273
+
2274
+ // src/member-content.ts
2275
+ var MEMBER_CONTENT_SCHEMA_VERSION = 1;
2276
+ var MEMBER_CONTENT_PATH = "/api/member-content";
2277
+ var MEMBER_CONTENT_CONTROL_PATH = "/api/member-content-control";
2278
+ var pageOrder = (a, b) => a.navigation.section.localeCompare(b.navigation.section) || a.navigation.order - b.navigation.order || a.title.localeCompare(b.title) || a.id.localeCompare(b.id);
2279
+ function composeMemberContent(parent, chapter) {
2280
+ const pages = [];
2281
+ const parentPages = /* @__PURE__ */ new Map();
2282
+ for (const page of parent?.pages ?? []) {
2283
+ const effective = { ...page, source: parent.source };
2284
+ parentPages.set(page.id, effective);
2285
+ pages.push(effective);
2286
+ }
2287
+ for (const override of chapter?.upsellOverrides ?? []) {
2288
+ const page = parentPages.get(override.pageId);
2289
+ if (page?.upsell?.owner === "chapter") page.upsell = { owner: "chapter", offer: override.offer };
2290
+ }
2291
+ for (const page of chapter?.pages ?? []) {
2292
+ if (parentPages.has(page.id) || pages.some((candidate) => candidate.slug === page.slug)) {
2293
+ throw new TypeError(`Chapter member page "${page.id}" conflicts with parent content`);
2294
+ }
2295
+ pages.push({ ...page, source: chapter.source });
2296
+ }
2297
+ return {
2298
+ pages: pages.sort(pageOrder),
2299
+ parentRevision: parent?.revision ?? null,
2300
+ chapterRevision: chapter?.revision ?? null
2301
+ };
2302
+ }
2303
+ var allowed = (page, viewer) => viewer.admin || page.audience.kind === "all-members" || viewer.tierId !== null && page.audience.tierIds.includes(viewer.tierId);
2304
+ function projectMemberContent(content, viewer, tiers2) {
2305
+ const authority = new Map(tiers2.filter((tier) => tier.active !== false).map((tier) => [tier.id, tier]));
2306
+ return {
2307
+ viewer: { ...viewer },
2308
+ revisions: { parent: content.parentRevision, chapter: content.chapterRevision },
2309
+ pages: content.pages.map((page) => {
2310
+ const canRead = allowed(page, viewer);
2311
+ const offer = !canRead ? page.upsell?.offer : void 0;
2312
+ const tier = offer ? authority.get(offer.tierId) : void 0;
2313
+ const upsell = offer && tier && typeof tier.name === "string" && typeof tier.priceCents === "number" ? { ...offer, tierName: tier.name, priceCents: tier.priceCents } : void 0;
2314
+ return {
2315
+ id: page.id,
2316
+ slug: page.slug,
2317
+ title: page.title,
2318
+ ...page.summary ? { summary: page.summary } : {},
2319
+ navigation: page.navigation,
2320
+ audience: page.audience,
2321
+ source: page.source,
2322
+ access: canRead ? "allowed" : "locked",
2323
+ ...page.teaserMarkdown ? { teaserMarkdown: page.teaserMarkdown } : {},
2324
+ ...canRead ? { bodyMarkdown: page.bodyMarkdown } : {},
2325
+ ...upsell ? { upsell } : {}
2326
+ };
2327
+ })
2328
+ };
2329
+ }
2330
+
2331
+ // src/member-content-canonical.ts
2332
+ var encoder2 = new TextEncoder();
2333
+ function canonicalValue2(value) {
2334
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2335
+ return JSON.stringify(value);
2336
+ }
2337
+ if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
2338
+ if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
2339
+ const record3 = value;
2340
+ const entries = Object.keys(record3).sort().flatMap((key) => {
2341
+ const item = record3[key];
2342
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
2343
+ });
2344
+ return `{${entries.join(",")}}`;
2345
+ }
2346
+ function canonicalMemberContent(value) {
2347
+ return canonicalValue2(value);
2348
+ }
2349
+ async function memberContentDigest(document) {
2350
+ const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
2351
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2352
+ }
2353
+ async function createMemberContentEnvelope(document) {
2354
+ return { document, digest: await memberContentDigest(document) };
2355
+ }
2356
+
1550
2357
  // src/email.ts
1551
2358
  function render(template, vars) {
1552
2359
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
@@ -1577,10 +2384,10 @@ function planDelivery(input) {
1577
2384
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1578
2385
  const to = redirect ? input.group.debugEmail : input.to;
1579
2386
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1580
- const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
2387
+ const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
1581
2388
 
1582
2389
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1583
- return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
2390
+ return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
1584
2391
  }
1585
2392
 
1586
2393
  // src/notify.ts
@@ -1615,9 +2422,9 @@ async function sendTemplated(deps, input) {
1615
2422
  error = e instanceof Error ? e.message : String(e);
1616
2423
  }
1617
2424
  }
1618
- const id2 = deps.newId();
2425
+ const id4 = deps.newId();
1619
2426
  const row = {
1620
- id: id2,
2427
+ id: id4,
1621
2428
  groupId: input.group.id,
1622
2429
  to: decision.to,
1623
2430
  template: input.template,
@@ -1631,7 +2438,7 @@ async function sendTemplated(deps, input) {
1631
2438
  ...messageId ? { messageId } : {},
1632
2439
  ...error ? { error } : {}
1633
2440
  };
1634
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id2, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
2441
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id4, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
1635
2442
  return error ? { sent: false, reason: error } : { sent: true };
1636
2443
  }
1637
2444
  function emailGroupFrom(row) {
@@ -1667,8 +2474,26 @@ function findApplicationRef(obj) {
1667
2474
  const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
1668
2475
  return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
1669
2476
  }
2477
+ function findGiftRef(obj) {
2478
+ const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
2479
+ const pick = (metadata) => typeof metadata.giftIntentId === "string" ? metadata.giftIntentId : void 0;
2480
+ const parent = obj.parent;
2481
+ const subscription = obj.subscription_details ?? parent?.subscription_details;
2482
+ const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
2483
+ const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
2484
+ return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
2485
+ }
1670
2486
  function normalizeWebhookEvent(event) {
1671
2487
  const obj = event.data?.object ?? {};
2488
+ const gift = findGiftRef(obj);
2489
+ if (gift.giftIntentId) {
2490
+ if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
2491
+ return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
2492
+ }
2493
+ if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
2494
+ if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
2495
+ return { kind: "ignored", type: event.type };
2496
+ }
1672
2497
  const ref = findApplicationRef(obj);
1673
2498
  switch (event.type) {
1674
2499
  case "invoice.paid": {
@@ -1702,6 +2527,112 @@ function canceledPatch() {
1702
2527
  return { canceled: true };
1703
2528
  }
1704
2529
 
2530
+ // src/join-continuation.ts
2531
+ function resolveJoinContinuation(input) {
2532
+ if (input.status === "approved") return "accepted";
2533
+ if (input.interviewWaived) {
2534
+ if (!input.payable) return "approve";
2535
+ if (input.status === input.initialStatus) {
2536
+ return input.subscriptionStarted ? "paymentPending" : "payment";
2537
+ }
2538
+ return "approve";
2539
+ }
2540
+ if (!input.payable) return "booking";
2541
+ if (input.status === input.initialStatus) {
2542
+ return input.subscriptionStarted ? "paymentPending" : "payment";
2543
+ }
2544
+ if (canBook(input.status, input.pipeline)) return "booking";
2545
+ return null;
2546
+ }
2547
+
2548
+ // src/gift-offers.ts
2549
+ var GIFT_OFFER_CATALOG_VERSION = 1;
2550
+ var MAX_GIFT_OFFERS = 20;
2551
+ var ID3 = /^[a-z][a-z0-9-]{1,79}$/;
2552
+ var PRICE = /^price_[A-Za-z0-9]{12,}$/;
2553
+ var CURRENCY = /^[a-z]{3}$/;
2554
+ var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
2555
+ var text = (value, path, errors, max) => {
2556
+ if (typeof value !== "string" || !value.trim()) {
2557
+ errors.push(`${path} is required`);
2558
+ return "";
2559
+ }
2560
+ if (value.trim().length > max) errors.push(`${path} must be at most ${max} characters`);
2561
+ return value.trim();
2562
+ };
2563
+ function validateGiftOfferCatalog(input, tierIds) {
2564
+ const errors = [];
2565
+ const root = record2(input);
2566
+ if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
2567
+ for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
2568
+ if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
2569
+ if (!Array.isArray(root.offers)) errors.push("document.giftOffers.offers must be an array");
2570
+ const rawOffers = Array.isArray(root.offers) ? root.offers : [];
2571
+ if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
2572
+ const ids = /* @__PURE__ */ new Set();
2573
+ const offers = [];
2574
+ for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
2575
+ const path = `document.giftOffers.offers[${index}]`;
2576
+ const item = record2(raw);
2577
+ if (!item) {
2578
+ errors.push(`${path} must be an object`);
2579
+ continue;
2580
+ }
2581
+ const allowed2 = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
2582
+ for (const key of Object.keys(item)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2583
+ const id4 = text(item.id, `${path}.id`, errors, 80);
2584
+ if (id4 && !ID3.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2585
+ if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
2586
+ ids.add(id4);
2587
+ const targetRaw = record2(item.target);
2588
+ let target = { type: "entitlement", entitlementKey: "invalid" };
2589
+ if (targetRaw?.type === "tier") {
2590
+ const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
2591
+ if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
2592
+ target = { type: "tier", tierId };
2593
+ } else if (targetRaw?.type === "entitlement") {
2594
+ const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
2595
+ if (entitlementKey && !ID3.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
2596
+ target = { type: "entitlement", entitlementKey };
2597
+ } else errors.push(`${path}.target.type must be tier or entitlement`);
2598
+ if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
2599
+ const stripePriceId = text(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
2600
+ if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2601
+ 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`);
2602
+ const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
2603
+ if (!CURRENCY.test(currency)) errors.push(`${path}.currency must be a lowercase ISO currency code`);
2604
+ const lifetime = Number(item.redemptionLifetimeHours);
2605
+ if (!Number.isSafeInteger(lifetime) || lifetime < 1 || lifetime > 8760) errors.push(`${path}.redemptionLifetimeHours must be an integer from 1 to 8760`);
2606
+ const activeFrom = item.activeFrom === void 0 ? void 0 : Number(item.activeFrom);
2607
+ const activeUntil = item.activeUntil === void 0 ? void 0 : Number(item.activeUntil);
2608
+ if (activeFrom !== void 0 && (!Number.isSafeInteger(activeFrom) || activeFrom < 1)) errors.push(`${path}.activeFrom must be epoch milliseconds`);
2609
+ if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
2610
+ if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
2611
+ offers.push({
2612
+ id: id4,
2613
+ target,
2614
+ billingMode: item.billingMode,
2615
+ stripePriceId,
2616
+ priceCents: Number(item.priceCents),
2617
+ currency,
2618
+ redemptionLifetimeHours: lifetime,
2619
+ ...activeFrom !== void 0 ? { activeFrom } : {},
2620
+ ...activeUntil !== void 0 ? { activeUntil } : {},
2621
+ title: text(item.title, `${path}.title`, errors, 160),
2622
+ description: text(item.description, `${path}.description`, errors, 2e3),
2623
+ ctaLabel: text(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
2624
+ });
2625
+ }
2626
+ return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
2627
+ }
2628
+ var giftOfferActive = (offer, now) => (!offer.activeFrom || offer.activeFrom <= now) && (!offer.activeUntil || offer.activeUntil > now);
2629
+ function publicGiftOffers(catalog, now) {
2630
+ return (catalog?.offers ?? []).filter((offer) => giftOfferActive(offer, now)).map(({ stripePriceId: _stripe, target: _target, ...offer }) => offer);
2631
+ }
2632
+ function findGiftOffer(catalog, offerId, now) {
2633
+ return catalog?.offers.find((offer) => offer.id === offerId && giftOfferActive(offer, now)) ?? null;
2634
+ }
2635
+
1705
2636
  // src/network.ts
1706
2637
  import {
1707
2638
  createRecord,
@@ -1746,34 +2677,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
1746
2677
  if (readable && raw.length <= 64) return raw;
1747
2678
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
1748
2679
  }
1749
- function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1750
- if ("input" in record && record.version === 2) {
2680
+ function normalizeSharedRecord(record3, fallbackSourceId = "legacy-source") {
2681
+ if ("input" in record3 && record3.version === 2) {
1751
2682
  return {
1752
2683
  version: 2,
1753
- sourceId: record.source.siteId,
1754
- sourceRecordId: record.source.recordId,
1755
- type: record.type,
1756
- input: record.input
2684
+ sourceId: record3.source.siteId,
2685
+ sourceRecordId: record3.source.recordId,
2686
+ type: record3.type,
2687
+ input: record3.input
1757
2688
  };
1758
2689
  }
1759
- if ("input" in record) {
2690
+ if ("input" in record3) {
1760
2691
  return {
1761
2692
  version: 1,
1762
2693
  sourceId: fallbackSourceId,
1763
- sourceRecordId: record.hubRecordId,
1764
- type: record.type,
1765
- input: record.input
2694
+ sourceRecordId: record3.hubRecordId,
2695
+ type: record3.type,
2696
+ input: record3.input
1766
2697
  };
1767
2698
  }
1768
- if ("type" in record && record.type === "company") {
1769
- const input = { name: record.name };
2699
+ if ("type" in record3 && record3.type === "company") {
2700
+ const input = { name: record3.name };
1770
2701
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
1771
- if (record[key]) input[key] = record[key];
2702
+ if (record3[key]) input[key] = record3[key];
1772
2703
  }
1773
2704
  return {
1774
2705
  version: 1,
1775
2706
  sourceId: fallbackSourceId,
1776
- sourceRecordId: record.hubRecordId,
2707
+ sourceRecordId: record3.hubRecordId,
1777
2708
  type: "company",
1778
2709
  input
1779
2710
  };
@@ -1781,28 +2712,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1781
2712
  return {
1782
2713
  version: 1,
1783
2714
  sourceId: fallbackSourceId,
1784
- sourceRecordId: record.hubRecordId,
2715
+ sourceRecordId: record3.hubRecordId,
1785
2716
  type: "person",
1786
- input: sharedPersonInput(record)
2717
+ input: sharedPersonInput(record3)
1787
2718
  };
1788
2719
  }
1789
- function sharedRecordFromCrm(crm, record, target, sourceId) {
1790
- if (target.fields && !target.fields[record.type]) {
1791
- throw new Error(`${target.name} does not accept "${record.type}" records`);
2720
+ function sharedRecordFromCrm(crm, record3, target, sourceId) {
2721
+ if (target.fields && !target.fields[record3.type]) {
2722
+ throw new Error(`${target.name} does not accept "${record3.type}" records`);
1792
2723
  }
1793
- const def = crm.type(record.type);
1794
- const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
2724
+ const def = crm.type(record3.type);
2725
+ const fields = target.fields?.[record3.type] ?? DEFAULT_SHARE_FIELDS[record3.type];
1795
2726
  if (!fields) {
1796
- throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
2727
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record3.type}" records`);
1797
2728
  }
1798
2729
  const nameField = def.nameField ?? "name";
1799
2730
  const input = {};
1800
2731
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
1801
- const value = record.fields?.[field];
2732
+ const value = record3.fields?.[field];
1802
2733
  if (value !== void 0) input[field] = value;
1803
2734
  }
1804
- if (input[nameField] === void 0) input[nameField] = record.name;
1805
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
2735
+ if (input[nameField] === void 0) input[nameField] = record3.name;
2736
+ return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record3.id }, type: record3.type, input } : { version: 1, type: record3.type, hubRecordId: record3.id, input };
1806
2737
  }
1807
2738
  async function upsertPerson(deps, opts) {
1808
2739
  const email = opts.email.toLowerCase();
@@ -1816,11 +2747,11 @@ async function upsertPerson(deps, opts) {
1816
2747
  const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
1817
2748
  return { recordId: created.id };
1818
2749
  }
1819
- async function findSharedRecord(deps, record, tag) {
2750
+ async function findSharedRecord(deps, record3, tag) {
1820
2751
  const structured = await getRecordByOrigin(
1821
2752
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
1822
- record.sourceId,
1823
- record.sourceRecordId
2753
+ record3.sourceId,
2754
+ record3.sourceRecordId
1824
2755
  );
1825
2756
  if (structured) return structured;
1826
2757
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -1829,53 +2760,53 @@ async function findSharedRecord(deps, record, tag) {
1829
2760
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1830
2761
  if (found.crm_record?.[0]) return found.crm_record[0];
1831
2762
  }
1832
- const def = deps.crm.type(record.type);
2763
+ const def = deps.crm.type(record3.type);
1833
2764
  const emailField = def.emailField;
1834
- if (emailField && typeof record.input[emailField] === "string") {
1835
- const primaryEmail = record.input[emailField].toLowerCase();
2765
+ if (emailField && typeof record3.input[emailField] === "string") {
2766
+ const primaryEmail = record3.input[emailField].toLowerCase();
1836
2767
  const found = await deps.db.query({
1837
- crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
2768
+ crm_record: { $: { where: { type: record3.type, primaryEmail }, limit: 1 } }
1838
2769
  });
1839
2770
  if (found.crm_record?.[0]) return found.crm_record[0];
1840
2771
  }
1841
- const domain = record.input.domain;
2772
+ const domain = record3.input.domain;
1842
2773
  const domainSlot = def.fields.domain?.slot;
1843
2774
  if (typeof domain === "string" && domainSlot) {
1844
2775
  const found = await deps.db.query({
1845
- crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
2776
+ crm_record: { $: { where: { type: record3.type, [domainSlot]: domain }, limit: 1 } }
1846
2777
  });
1847
2778
  if (found.crm_record?.[0]) return found.crm_record[0];
1848
2779
  }
1849
2780
  const nameField = def.nameField ?? "name";
1850
- const name = record.input[nameField];
1851
- if (record.type === "company" && typeof name === "string" && name.trim()) {
2781
+ const name = record3.input[nameField];
2782
+ if (record3.type === "company" && typeof name === "string" && name.trim()) {
1852
2783
  const found = await deps.db.query({
1853
- crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
2784
+ crm_record: { $: { where: { type: record3.type, name: name.trim() }, limit: 1 } }
1854
2785
  });
1855
2786
  if (found.crm_record?.[0]) return found.crm_record[0];
1856
2787
  }
1857
2788
  return void 0;
1858
2789
  }
1859
2790
  async function projectSharedRecord(deps, shared, options = {}) {
1860
- const record = normalizeSharedRecord(shared, options.sourceId);
1861
- if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
2791
+ const record3 = normalizeSharedRecord(shared, options.sourceId);
2792
+ if (!record3.type.trim() || !record3.sourceId.trim() || !record3.sourceRecordId.trim()) {
1862
2793
  throw new Error("type, source site id, and source record id must be non-empty");
1863
2794
  }
1864
- if (options.sourceId && options.sourceId !== record.sourceId) {
2795
+ if (options.sourceId && options.sourceId !== record3.sourceId) {
1865
2796
  throw new Error("signed sender does not match payload source");
1866
2797
  }
1867
- const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
2798
+ const tag = networkSourceTag(record3.type, record3.sourceRecordId, record3.version === 2 ? record3.sourceId : void 0);
1868
2799
  const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1869
- const existing = await findSharedRecord(deps, record, tag);
2800
+ const existing = await findSharedRecord(deps, record3, tag);
1870
2801
  let recordId;
1871
2802
  if (existing && typeof existing.id === "string") {
1872
- await updateRecord(crmDeps, { id: existing.id, input: record.input });
2803
+ await updateRecord(crmDeps, { id: existing.id, input: record3.input });
1873
2804
  recordId = existing.id;
1874
2805
  } else {
1875
- recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
2806
+ recordId = `network_${shortHash(`${record3.sourceId}\0${record3.type}\0${record3.sourceRecordId}`)}`;
1876
2807
  await createRecord({ ...crmDeps, newId: () => recordId }, {
1877
- type: record.type,
1878
- input: record.input,
2808
+ type: record3.type,
2809
+ input: record3.input,
1879
2810
  mutationId: `share-create:${tag}`
1880
2811
  });
1881
2812
  }
@@ -1885,10 +2816,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
1885
2816
  );
1886
2817
  await upsertRecordOrigin(crmDeps, {
1887
2818
  recordId,
1888
- sourceId: record.sourceId,
1889
- sourceRecordId: record.sourceRecordId,
2819
+ sourceId: record3.sourceId,
2820
+ sourceRecordId: record3.sourceRecordId,
1890
2821
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
1891
- payloadVersion: record.version
2822
+ payloadVersion: record3.version
1892
2823
  });
1893
2824
  return { recordId };
1894
2825
  }
@@ -2104,9 +3035,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
2104
3035
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
2105
3036
  if (!found.ok) return false;
2106
3037
  const users = await found.json().catch(() => null);
2107
- const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
2108
- if (!id2) return false;
2109
- return updateClerkUserMetadata(secretKey, id2, input, fetchImpl);
3038
+ const id4 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
3039
+ if (!id4) return false;
3040
+ return updateClerkUserMetadata(secretKey, id4, input, fetchImpl);
2110
3041
  }
2111
3042
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
2112
3043
  const { path, body } = clerkUserRequest(input);
@@ -2162,8 +3093,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
2162
3093
  const user = Array.isArray(data) ? data[0] : void 0;
2163
3094
  return user ? toRecord(user) : null;
2164
3095
  }
2165
- async function clerkGetUser(secretKey, id2, fetchImpl = fetch) {
2166
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id2)}`, secretKey, fetchImpl).catch(() => null);
3096
+ async function clerkGetUser(secretKey, id4, fetchImpl = fetch) {
3097
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id4)}`, secretKey, fetchImpl).catch(() => null);
2167
3098
  return data ? toRecord(data) : null;
2168
3099
  }
2169
3100
  async function clerkListUsers(secretKey, fetchImpl = fetch) {
@@ -2172,15 +3103,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
2172
3103
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
2173
3104
  const page = Array.isArray(data) ? data : [];
2174
3105
  for (const u of page) {
2175
- const record = toRecord(u);
2176
- if (record) out.push(record);
3106
+ const record3 = toRecord(u);
3107
+ if (record3) out.push(record3);
2177
3108
  }
2178
3109
  if (page.length < PAGE) break;
2179
3110
  }
2180
3111
  return out;
2181
3112
  }
2182
- async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
2183
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id2)}/metadata`, {
3113
+ async function clerkSetRole(secretKey, id4, role, fetchImpl = fetch) {
3114
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id4)}/metadata`, {
2184
3115
  method: "PATCH",
2185
3116
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
2186
3117
  body: JSON.stringify({ public_metadata: { role } })
@@ -2275,7 +3206,8 @@ function applicationSummary(app) {
2275
3206
  meetingLink: app.meetingLink ?? null,
2276
3207
  paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
2277
3208
  renewalAt: app.renewalAt ?? null,
2278
- canceled: app.canceled === true
3209
+ canceled: app.canceled === true,
3210
+ tierId: app.tierId ?? null
2279
3211
  };
2280
3212
  }
2281
3213
  function memberApplication(app, meeting, defaultTimezone) {
@@ -2556,15 +3488,15 @@ function rowToTier(row, groupId) {
2556
3488
  }
2557
3489
  async function loadTiers(db, group) {
2558
3490
  const groupId = String(group.id ?? "");
2559
- let rows;
3491
+ let rows2;
2560
3492
  try {
2561
- rows = (await db.query({
3493
+ rows2 = (await db.query({
2562
3494
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
2563
3495
  })).tiers;
2564
3496
  } catch {
2565
- rows = void 0;
3497
+ rows2 = void 0;
2566
3498
  }
2567
- const list = Array.isArray(rows) ? rows.map((row) => rowToTier(row, groupId)) : [];
3499
+ const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
2568
3500
  return resolveTiers(group, list);
2569
3501
  }
2570
3502
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -2575,37 +3507,11 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
2575
3507
  var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
2576
3508
  var SIGNUP_CONTROL_PATH = "/api/signup-control";
2577
3509
 
2578
- // src/signup-control-canonical.ts
2579
- var encoder = new TextEncoder();
2580
- function canonicalValue(value) {
2581
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2582
- return JSON.stringify(value);
2583
- }
2584
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
2585
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
2586
- const record = value;
2587
- const entries = Object.keys(record).sort().flatMap((key) => {
2588
- const item = record[key];
2589
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
2590
- });
2591
- return `{${entries.join(",")}}`;
2592
- }
2593
- function canonicalSignupControl(value) {
2594
- return canonicalValue(value);
2595
- }
2596
- async function signupControlDigest(document) {
2597
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
2598
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2599
- }
2600
- async function createSignupControlEnvelope(document) {
2601
- return { document, digest: await signupControlDigest(document) };
2602
- }
2603
-
2604
3510
  // src/membership-page.ts
2605
3511
  var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
2606
3512
  var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
2607
3513
  var VARIANTS = /* @__PURE__ */ new Set(["standard", "featured", "supporting"]);
2608
- var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
3514
+ var ROOT_KEYS2 = /* @__PURE__ */ new Set(["schemaVersion", "renderer", "tiers", "footnoteMarkdown"]);
2609
3515
  var TIER_KEYS = /* @__PURE__ */ new Set([
2610
3516
  "tierId",
2611
3517
  "variant",
@@ -2624,12 +3530,12 @@ var CALLOUT_KEYS = /* @__PURE__ */ new Set(["label", "bodyMarkdown"]);
2624
3530
  var CAPACITY_KEYS = /* @__PURE__ */ new Set(["total", "label"]);
2625
3531
  var MAX_TIERS = 20;
2626
3532
  var MAX_BENEFITS = 24;
2627
- var MAX_MARKDOWN = 65536;
3533
+ var MAX_MARKDOWN2 = 65536;
2628
3534
  var MAX_LABEL = 160;
2629
3535
  var objectRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2630
- function rejectUnknownKeys(value, allowed, path, errors) {
3536
+ function rejectUnknownKeys(value, allowed2, path, errors) {
2631
3537
  for (const key of Object.keys(value)) {
2632
- if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
3538
+ if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2633
3539
  }
2634
3540
  }
2635
3541
  function boundedString(value, path, errors, max, required = false) {
@@ -2641,16 +3547,16 @@ function boundedString(value, path, errors, max, required = false) {
2641
3547
  errors.push(`${path} must be a string`);
2642
3548
  return void 0;
2643
3549
  }
2644
- const text2 = value.trim();
2645
- if (required && !text2) errors.push(`${path} is required`);
2646
- if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2647
- return text2;
3550
+ const text3 = value.trim();
3551
+ if (required && !text3) errors.push(`${path} is required`);
3552
+ if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
3553
+ return text3;
2648
3554
  }
2649
3555
  function validateMembershipPageDocument(input, options = {}) {
2650
3556
  const errors = [];
2651
3557
  const root = objectRecord(input);
2652
3558
  if (!root) return { ok: false, errors: ["document must be an object"] };
2653
- rejectUnknownKeys(root, ROOT_KEYS, "document", errors);
3559
+ rejectUnknownKeys(root, ROOT_KEYS2, "document", errors);
2654
3560
  if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
2655
3561
  if (root.renderer !== MEMBERSHIP_PAGE_RENDERER) {
2656
3562
  errors.push(`document.renderer must be "${MEMBERSHIP_PAGE_RENDERER}"`);
@@ -2693,8 +3599,8 @@ function validateMembershipPageDocument(input, options = {}) {
2693
3599
  else {
2694
3600
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
2695
3601
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
2696
- const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2697
- return text2 ? [text2] : [];
3602
+ const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
3603
+ return text3 ? [text3] : [];
2698
3604
  });
2699
3605
  }
2700
3606
  }
@@ -2704,7 +3610,7 @@ function validateMembershipPageDocument(input, options = {}) {
2704
3610
  if (!rawCallout) errors.push(`${path}.callout must be an object`);
2705
3611
  else {
2706
3612
  rejectUnknownKeys(rawCallout, CALLOUT_KEYS, `${path}.callout`, errors);
2707
- const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN, true);
3613
+ const bodyMarkdown = boundedString(rawCallout.bodyMarkdown, `${path}.callout.bodyMarkdown`, errors, MAX_MARKDOWN2, true);
2708
3614
  const label = boundedString(rawCallout.label, `${path}.callout.label`, errors, MAX_LABEL);
2709
3615
  if (bodyMarkdown) callout = { ...label ? { label } : {}, bodyMarkdown };
2710
3616
  }
@@ -2726,10 +3632,10 @@ function validateMembershipPageDocument(input, options = {}) {
2726
3632
  }
2727
3633
  }
2728
3634
  const badge = boundedString(item.badge, `${path}.badge`, errors, MAX_LABEL);
2729
- const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN);
3635
+ const summaryMarkdown = boundedString(item.summaryMarkdown, `${path}.summaryMarkdown`, errors, MAX_MARKDOWN2);
2730
3636
  const priceEyebrow = boundedString(item.priceEyebrow, `${path}.priceEyebrow`, errors, MAX_LABEL);
2731
3637
  const billingLabel = boundedString(item.billingLabel, `${path}.billingLabel`, errors, MAX_LABEL);
2732
- const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN);
3638
+ const priceNoteMarkdown = boundedString(item.priceNoteMarkdown, `${path}.priceNoteMarkdown`, errors, MAX_MARKDOWN2);
2733
3639
  const ctaLabel = boundedString(item.ctaLabel, `${path}.ctaLabel`, errors, MAX_LABEL);
2734
3640
  tiers2.push({
2735
3641
  tierId,
@@ -2751,7 +3657,7 @@ function validateMembershipPageDocument(input, options = {}) {
2751
3657
  if (!seen.has(tierId)) errors.push(`document.tiers is missing offered tier "${tierId}"`);
2752
3658
  }
2753
3659
  }
2754
- const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN);
3660
+ const footnoteMarkdown = boundedString(root.footnoteMarkdown, "document.footnoteMarkdown", errors, MAX_MARKDOWN2);
2755
3661
  if (errors.length) return { ok: false, errors };
2756
3662
  return {
2757
3663
  ok: true,
@@ -2770,11 +3676,11 @@ function membershipPageDocument(input, options = {}) {
2770
3676
  }
2771
3677
 
2772
3678
  // src/signup-control-validate.ts
2773
- var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
3679
+ var SLUG6 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2774
3680
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
2775
3681
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
2776
- var PRICE = /^price_[A-Za-z0-9]{12,}$/;
2777
- var ROOT_KEYS2 = /* @__PURE__ */ new Set([
3682
+ var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
3683
+ var ROOT_KEYS3 = /* @__PURE__ */ new Set([
2778
3684
  "schemaVersion",
2779
3685
  "revision",
2780
3686
  "generatedAt",
@@ -2785,14 +3691,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2785
3691
  "application",
2786
3692
  "policy",
2787
3693
  "scheduling",
2788
- "theme"
3694
+ "theme",
3695
+ "giftOffers"
2789
3696
  ]);
2790
3697
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2791
3698
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2792
- function unknownKeys(value, allowed, path, errors) {
2793
- for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
3699
+ function unknownKeys2(value, allowed2, path, errors) {
3700
+ for (const key of Object.keys(value)) if (!allowed2.has(key)) errors.push(`${path}.${key} is not supported`);
2794
3701
  }
2795
- function text(value, path, errors, max = 65536) {
3702
+ function text2(value, path, errors, max = 65536) {
2796
3703
  if (typeof value !== "string" || !value.trim()) {
2797
3704
  errors.push(`${path} is required`);
2798
3705
  return "";
@@ -2814,26 +3721,26 @@ function tiersOf(value, errors) {
2814
3721
  errors.push(`${path} must be an object`);
2815
3722
  return [];
2816
3723
  }
2817
- unknownKeys(item, TIER_KEYS2, path, errors);
2818
- const id2 = text(item.id, `${path}.id`, errors, 80);
2819
- if (id2 && !TIER.test(id2)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2820
- if (seen.has(id2)) errors.push(`${path}.id duplicates "${id2}"`);
2821
- seen.add(id2);
2822
- const name = text(item.name, `${path}.name`, errors, 160);
3724
+ unknownKeys2(item, TIER_KEYS2, path, errors);
3725
+ const id4 = text2(item.id, `${path}.id`, errors, 80);
3726
+ if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3727
+ if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
3728
+ seen.add(id4);
3729
+ const name = text2(item.name, `${path}.name`, errors, 160);
2823
3730
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
2824
3731
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
2825
3732
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
2826
3733
  errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
2827
3734
  }
2828
- if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
3735
+ if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
2829
3736
  errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2830
3737
  }
2831
3738
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
2832
3739
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
2833
3740
  }
2834
- const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
3741
+ const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
2835
3742
  return [{
2836
- id: id2,
3743
+ id: id4,
2837
3744
  name,
2838
3745
  available: item.available === true,
2839
3746
  sortOrder: Number(item.sortOrder) || 0,
@@ -2850,7 +3757,7 @@ function applicationOf(value, errors) {
2850
3757
  errors.push("document.application must be an object");
2851
3758
  return void 0;
2852
3759
  }
2853
- unknownKeys(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
3760
+ unknownKeys2(input, /* @__PURE__ */ new Set(["required", "optional", "conditions", "copy"]), "document.application", errors);
2854
3761
  try {
2855
3762
  const resolved = resolveApplication(input);
2856
3763
  const copy = recordOf(input.copy);
@@ -2875,12 +3782,12 @@ function policyOf(value, errors) {
2875
3782
  errors.push("document.policy must be an object");
2876
3783
  return void 0;
2877
3784
  }
2878
- const allowed = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
2879
- unknownKeys(input, allowed, "document.policy", errors);
3785
+ const allowed2 = /* @__PURE__ */ new Set(["disclaimerText", "refundPolicyText", "trustCopy", "commitmentText", "normsText"]);
3786
+ unknownKeys2(input, allowed2, "document.policy", errors);
2880
3787
  const result = {};
2881
- for (const key of allowed) {
3788
+ for (const key of allowed2) {
2882
3789
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
2883
- result[key] = text(input[key], `document.policy.${key}`, errors);
3790
+ result[key] = text2(input[key], `document.policy.${key}`, errors);
2884
3791
  }
2885
3792
  return result;
2886
3793
  }
@@ -2892,7 +3799,7 @@ function themeOf(value, errors) {
2892
3799
  errors.push("document.theme.tokens must be an object");
2893
3800
  return void 0;
2894
3801
  }
2895
- unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
3802
+ unknownKeys2(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
2896
3803
  if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
2897
3804
  let serializedLength = 0;
2898
3805
  for (const [key, token] of Object.entries(tokens)) {
@@ -2912,17 +3819,17 @@ function validateSignupControlDocument(input) {
2912
3819
  const errors = [];
2913
3820
  const root = recordOf(input);
2914
3821
  if (!root) return { ok: false, errors: ["document must be an object"] };
2915
- unknownKeys(root, ROOT_KEYS2, "document", errors);
3822
+ unknownKeys2(root, ROOT_KEYS3, "document", errors);
2916
3823
  if (root.schemaVersion !== 1) errors.push("document.schemaVersion must be 1");
2917
3824
  if (!Number.isSafeInteger(root.revision) || Number(root.revision) < 1) errors.push("document.revision must be a positive integer");
2918
3825
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
2919
3826
  const source = recordOf(root.source);
2920
3827
  const target = recordOf(root.target);
2921
- const appId = text(source?.appId, "document.source.appId", errors, 63);
2922
- const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
2923
- const environment = text(target?.environment, "document.target.environment", errors, 63);
2924
- if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
2925
- if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
3828
+ const appId = text2(source?.appId, "document.source.appId", errors, 63);
3829
+ const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
3830
+ const environment = text2(target?.environment, "document.target.environment", errors, 63);
3831
+ if (appId && !SLUG6.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
3832
+ if (chapterId && !SLUG6.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
2926
3833
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
2927
3834
  if (target?.stripeMode !== "test" && target?.stripeMode !== "live") errors.push("document.target.stripeMode must be test or live");
2928
3835
  const tiers2 = tiersOf(root.tiers, errors);
@@ -2941,6 +3848,8 @@ function validateSignupControlDocument(input) {
2941
3848
  else scheduling = root.scheduling;
2942
3849
  }
2943
3850
  const theme = themeOf(root.theme, errors);
3851
+ const giftOffers = root.giftOffers === void 0 ? void 0 : validateGiftOfferCatalog(root.giftOffers, new Set(tiers2.map((tier) => tier.id)));
3852
+ if (giftOffers && !giftOffers.ok) errors.push(...giftOffers.errors);
2944
3853
  if (errors.length || !page.ok) return { ok: false, errors };
2945
3854
  return { ok: true, value: {
2946
3855
  schemaVersion: 1,
@@ -2953,7 +3862,8 @@ function validateSignupControlDocument(input) {
2953
3862
  ...application ? { application } : {},
2954
3863
  ...policy ? { policy } : {},
2955
3864
  ...scheduling ? { scheduling } : {},
2956
- ...theme ? { theme } : {}
3865
+ ...theme ? { theme } : {},
3866
+ ...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
2957
3867
  } };
2958
3868
  }
2959
3869
 
@@ -3069,7 +3979,7 @@ function hydrateSignupControlTheme(target, revision, options = {}) {
3069
3979
 
3070
3980
  // src/membership-markdown.ts
3071
3981
  var MAX_BENEFITS2 = 24;
3072
- var MAX_MARKDOWN2 = 65536;
3982
+ var MAX_MARKDOWN3 = 65536;
3073
3983
  var MAX_LABEL2 = 160;
3074
3984
  var MARKDOWN_SECTION = /^##\s+(.+?)\s*$/;
3075
3985
  var MARKDOWN_HEADING = /^#{1,6}\s+/;
@@ -3094,17 +4004,17 @@ function markdownBenefits(lines, path, errors) {
3094
4004
  errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
3095
4005
  continue;
3096
4006
  }
3097
- const text2 = (match[1] ?? "").trim();
3098
- if (text2.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
3099
- if (text2) benefits.push(text2);
4007
+ const text3 = (match[1] ?? "").trim();
4008
+ if (text3.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
4009
+ if (text3) benefits.push(text3);
3100
4010
  }
3101
4011
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
3102
4012
  return benefits.slice(0, MAX_BENEFITS2);
3103
4013
  }
3104
4014
  function applyMembershipTierMarkdown(presentation, source) {
3105
4015
  if (typeof source !== "string") return { ok: false, errors: ["membership Markdown must be a string"] };
3106
- if (source.length > MAX_MARKDOWN2) {
3107
- return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN2.toLocaleString("en-US")} characters`] };
4016
+ if (source.length > MAX_MARKDOWN3) {
4017
+ return { ok: false, errors: [`membership Markdown must be at most ${MAX_MARKDOWN3.toLocaleString("en-US")} characters`] };
3108
4018
  }
3109
4019
  if (!source.trim()) return { ok: true, value: { ...presentation } };
3110
4020
  const errors = [];
@@ -3198,7 +4108,7 @@ var exclusions = (chapterId, paidTierIds) => [
3198
4108
  id: chapterId,
3199
4109
  reason: "provider_identity",
3200
4110
  paths: [
3201
- ...paidTierIds.map((id2) => `tiers[${id2}].stripePriceId`),
4111
+ ...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
3202
4112
  "groups.stripePublishableKey",
3203
4113
  "applications.stripeCustomerId",
3204
4114
  "applications.stripeSubscriptionId",
@@ -3240,9 +4150,9 @@ async function exportChapterPromotion(input) {
3240
4150
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
3241
4151
  receiptDigests: [input.digest]
3242
4152
  };
3243
- const artifact = (kind, id2, content) => ({
4153
+ const artifact = (kind, id4, content) => ({
3244
4154
  kind,
3245
- id: id2,
4155
+ id: id4,
3246
4156
  sourceRevision: revision,
3247
4157
  provenance: promotionProvenance,
3248
4158
  content: canonicalPromotionValue(content)
@@ -3280,10 +4190,17 @@ async function exportChapterPromotion(input) {
3280
4190
  };
3281
4191
  }
3282
4192
  export {
4193
+ ADMISSION_GRANTS_PATH,
4194
+ ADMISSION_GRANTS_SCHEMA_VERSION,
3283
4195
  CHAPTER_PROMOTION_ADAPTER,
3284
4196
  DEFAULT_CHAPTER_COPY,
3285
4197
  DEFAULT_SHARE_FIELDS,
4198
+ GIFT_OFFER_CATALOG_VERSION,
4199
+ MAX_GIFT_OFFERS,
3286
4200
  MEMBERSHIP_PAGE_RENDERER,
4201
+ MEMBER_CONTENT_CONTROL_PATH,
4202
+ MEMBER_CONTENT_PATH,
4203
+ MEMBER_CONTENT_SCHEMA_VERSION,
3287
4204
  SCHEDULING_DEFAULTS,
3288
4205
  SIGNUP_CONTROL_PATH,
3289
4206
  SIGNUP_CONTROL_SCHEMA_VERSION,
@@ -3304,6 +4221,7 @@ export {
3304
4221
  canChangeRole,
3305
4222
  canTransition,
3306
4223
  canceledPatch,
4224
+ canonicalMemberContent,
3307
4225
  canonicalSignupControl,
3308
4226
  chapterBrandFromTokens,
3309
4227
  chapterDb,
@@ -3315,9 +4233,12 @@ export {
3315
4233
  clerkListUsers,
3316
4234
  clerkSetRole,
3317
4235
  clerkUserRequest,
4236
+ composeMemberContent,
4237
+ createAdmissionGrantEnvelope,
3318
4238
  createChapterIntegration,
3319
4239
  createClerkInvitation,
3320
4240
  createClerkUser,
4241
+ createMemberContentEnvelope,
3321
4242
  createSignupControlEnvelope,
3322
4243
  dashboardMetricData,
3323
4244
  defaultCrm,
@@ -3325,12 +4246,15 @@ export {
3325
4246
  emailGroupFrom,
3326
4247
  endForSlot,
3327
4248
  exportChapterPromotion,
4249
+ findAdmissionGrant,
3328
4250
  findApplicationRef,
4251
+ findGiftOffer,
3329
4252
  findTier,
3330
4253
  firstPaymentPatch,
3331
4254
  formatChapterCopy,
3332
4255
  formationFields,
3333
4256
  getVaultSecret,
4257
+ giftOfferActive,
3334
4258
  hasDisclaimerAck,
3335
4259
  hydrateSignupControlTheme,
3336
4260
  injectSignupControlThemeHead,
@@ -3343,15 +4267,20 @@ export {
3343
4267
  joinConfig,
3344
4268
  joinConfigTiers,
3345
4269
  leaderCrmConfig,
4270
+ loadLatestAdmissionRevision,
3346
4271
  loadOfferableTiers,
3347
4272
  loadTiers,
4273
+ materializeAdmissionGrants,
3348
4274
  meetingCreateRow,
3349
4275
  meetingRescheduleUpdate,
3350
4276
  memberApplication,
4277
+ memberContentDigest,
4278
+ memberContentDocument,
3351
4279
  memberSession,
3352
4280
  membershipPageDocument,
3353
4281
  membershipTierPresentationFromMarkdown,
3354
4282
  networkSourceTag,
4283
+ normalizeAdmissionEmail,
3355
4284
  normalizeSharedRecord,
3356
4285
  normalizeWebhookEvent,
3357
4286
  offerableTiers,
@@ -3359,7 +4288,9 @@ export {
3359
4288
  personInputFromApp,
3360
4289
  planDelivery,
3361
4290
  projectApplicant,
4291
+ projectMemberContent,
3362
4292
  projectSharedRecord,
4293
+ publicGiftOffers,
3363
4294
  reconcileMeetings,
3364
4295
  refundedPatch,
3365
4296
  render,
@@ -3370,7 +4301,9 @@ export {
3370
4301
  resolveApplication,
3371
4302
  resolveAuth,
3372
4303
  resolveChapterCopy,
4304
+ resolveJoinContinuation,
3373
4305
  resolveLeaderFormation,
4306
+ resolveMemberContent,
3374
4307
  resolvePipeline,
3375
4308
  resolveScheduling,
3376
4309
  resolveSignupControl,
@@ -3392,6 +4325,9 @@ export {
3392
4325
  tierPayable,
3393
4326
  updateClerkUserMetadata,
3394
4327
  updateClerkUserMetadataByEmail,
4328
+ validateAdmissionGrantDocument,
4329
+ validateGiftOfferCatalog,
4330
+ validateMemberContentDocument,
3395
4331
  validateMembershipPageDocument,
3396
4332
  validateScheduling,
3397
4333
  validateSignupControlDocument,