@odla-ai/chapter 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,13 +20,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ADMISSION_GRANTS_PATH: () => ADMISSION_GRANTS_PATH,
24
+ ADMISSION_GRANTS_SCHEMA_VERSION: () => ADMISSION_GRANTS_SCHEMA_VERSION,
23
25
  CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
24
26
  DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
25
27
  DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
28
+ GIFT_OFFER_CATALOG_VERSION: () => GIFT_OFFER_CATALOG_VERSION,
29
+ MAX_GIFT_OFFERS: () => MAX_GIFT_OFFERS,
26
30
  MEMBERSHIP_PAGE_RENDERER: () => MEMBERSHIP_PAGE_RENDERER,
27
31
  SCHEDULING_DEFAULTS: () => SCHEDULING_DEFAULTS,
28
32
  SIGNUP_CONTROL_PATH: () => SIGNUP_CONTROL_PATH,
29
33
  SIGNUP_CONTROL_SCHEMA_VERSION: () => SIGNUP_CONTROL_SCHEMA_VERSION,
34
+ SIGNUP_CONTROL_THEME_MARKER: () => SIGNUP_CONTROL_THEME_MARKER,
35
+ SIGNUP_CONTROL_THEME_STYLE_ID: () => SIGNUP_CONTROL_THEME_STYLE_ID,
30
36
  applicantProfile: () => applicantProfile,
31
37
  applicationBookingUpdate: () => applicationBookingUpdate,
32
38
  applicationSummary: () => applicationSummary,
@@ -53,6 +59,7 @@ __export(index_exports, {
53
59
  clerkListUsers: () => clerkListUsers,
54
60
  clerkSetRole: () => clerkSetRole,
55
61
  clerkUserRequest: () => clerkUserRequest,
62
+ createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
56
63
  createChapterIntegration: () => createChapterIntegration,
57
64
  createClerkInvitation: () => createClerkInvitation,
58
65
  createClerkUser: () => createClerkUser,
@@ -63,13 +70,18 @@ __export(index_exports, {
63
70
  emailGroupFrom: () => emailGroupFrom,
64
71
  endForSlot: () => endForSlot,
65
72
  exportChapterPromotion: () => exportChapterPromotion,
73
+ findAdmissionGrant: () => findAdmissionGrant,
66
74
  findApplicationRef: () => findApplicationRef,
75
+ findGiftOffer: () => findGiftOffer,
67
76
  findTier: () => findTier,
68
77
  firstPaymentPatch: () => firstPaymentPatch,
69
78
  formatChapterCopy: () => formatChapterCopy,
70
79
  formationFields: () => formationFields,
71
80
  getVaultSecret: () => getVaultSecret,
81
+ giftOfferActive: () => giftOfferActive,
72
82
  hasDisclaimerAck: () => hasDisclaimerAck,
83
+ hydrateSignupControlTheme: () => hydrateSignupControlTheme,
84
+ injectSignupControlThemeHead: () => injectSignupControlThemeHead,
73
85
  introIdempotencyKey: () => introIdempotencyKey,
74
86
  isAdminRole: () => isAdminRole,
75
87
  isAlreadySent: () => isAlreadySent,
@@ -79,8 +91,10 @@ __export(index_exports, {
79
91
  joinConfig: () => joinConfig,
80
92
  joinConfigTiers: () => joinConfigTiers,
81
93
  leaderCrmConfig: () => leaderCrmConfig,
94
+ loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
82
95
  loadOfferableTiers: () => loadOfferableTiers,
83
96
  loadTiers: () => loadTiers,
97
+ materializeAdmissionGrants: () => materializeAdmissionGrants,
84
98
  meetingCreateRow: () => meetingCreateRow,
85
99
  meetingRescheduleUpdate: () => meetingRescheduleUpdate,
86
100
  memberApplication: () => memberApplication,
@@ -88,6 +102,7 @@ __export(index_exports, {
88
102
  membershipPageDocument: () => membershipPageDocument,
89
103
  membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
90
104
  networkSourceTag: () => networkSourceTag,
105
+ normalizeAdmissionEmail: () => normalizeAdmissionEmail,
91
106
  normalizeSharedRecord: () => normalizeSharedRecord,
92
107
  normalizeWebhookEvent: () => normalizeWebhookEvent,
93
108
  offerableTiers: () => offerableTiers,
@@ -96,15 +111,18 @@ __export(index_exports, {
96
111
  planDelivery: () => planDelivery,
97
112
  projectApplicant: () => projectApplicant,
98
113
  projectSharedRecord: () => projectSharedRecord,
114
+ publicGiftOffers: () => publicGiftOffers,
99
115
  reconcileMeetings: () => reconcileMeetings,
100
116
  refundedPatch: () => refundedPatch,
101
117
  render: () => render,
118
+ renderSignupControlThemeHead: () => renderSignupControlThemeHead,
102
119
  renderSummary: () => renderSummary,
103
120
  renderTemplateBody: () => renderTemplateBody,
104
121
  renewalPatch: () => renewalPatch,
105
122
  resolveApplication: () => resolveApplication,
106
123
  resolveAuth: () => resolveAuth,
107
124
  resolveChapterCopy: () => resolveChapterCopy,
125
+ resolveJoinContinuation: () => resolveJoinContinuation,
108
126
  resolveLeaderFormation: () => resolveLeaderFormation,
109
127
  resolvePipeline: () => resolvePipeline,
110
128
  resolveScheduling: () => resolveScheduling,
@@ -127,6 +145,8 @@ __export(index_exports, {
127
145
  tierPayable: () => tierPayable,
128
146
  updateClerkUserMetadata: () => updateClerkUserMetadata,
129
147
  updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
148
+ validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
149
+ validateGiftOfferCatalog: () => validateGiftOfferCatalog,
130
150
  validateMembershipPageDocument: () => validateMembershipPageDocument,
131
151
  validateScheduling: () => validateScheduling,
132
152
  validateSignupControlDocument: () => validateSignupControlDocument,
@@ -152,6 +172,213 @@ async function applicationIdForSubmission(submissionId) {
152
172
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
153
173
  }
154
174
 
175
+ // src/admission-grants-contract.ts
176
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
177
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
178
+
179
+ // src/signup-control-canonical.ts
180
+ var encoder = new TextEncoder();
181
+ function canonicalValue(value) {
182
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
183
+ return JSON.stringify(value);
184
+ }
185
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
186
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
187
+ const record2 = value;
188
+ const entries = Object.keys(record2).sort().flatMap((key) => {
189
+ const item = record2[key];
190
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
191
+ });
192
+ return `{${entries.join(",")}}`;
193
+ }
194
+ function canonicalSignupControl(value) {
195
+ return canonicalValue(value);
196
+ }
197
+ async function signupControlDigest(document) {
198
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
199
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
200
+ }
201
+ async function createSignupControlEnvelope(document) {
202
+ return { document, digest: await signupControlDigest(document) };
203
+ }
204
+
205
+ // src/admission-grants.ts
206
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
207
+ var MAX_GRANTS = 2e3;
208
+ var MAX_NOTE = 2e3;
209
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
210
+ var isAdmissionEmail = (value) => EMAIL_RE.test(value);
211
+ function normalizeAdmissionEmail(email) {
212
+ return email.trim().toLowerCase();
213
+ }
214
+ function validateAdmissionGrantDocument(input) {
215
+ const errors = [];
216
+ const root = input && typeof input === "object" ? input : {};
217
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
218
+ const revision = Number(root.revision);
219
+ const generatedAt = Number(root.generatedAt);
220
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
221
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
222
+ const source = root.source && typeof root.source === "object" ? root.source : {};
223
+ const target = root.target && typeof root.target === "object" ? root.target : {};
224
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
225
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
226
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
227
+ const environment = typeof target.environment === "string" ? target.environment : "";
228
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
229
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
230
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
231
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
232
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
233
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
234
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
235
+ const grants = [];
236
+ const seenIds = /* @__PURE__ */ new Set();
237
+ const seenEmails = /* @__PURE__ */ new Set();
238
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
239
+ const grant = raw && typeof raw === "object" ? raw : {};
240
+ const id4 = typeof grant.id === "string" ? grant.id : "";
241
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
242
+ const normalized = normalizeAdmissionEmail(email);
243
+ const decision = grant.decision;
244
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
245
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
246
+ if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
247
+ if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
248
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
249
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
250
+ if (decision !== "waive_interview" && decision !== "require_interview") {
251
+ errors.push(`document.grants[${index}].decision is invalid`);
252
+ }
253
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
254
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
255
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
256
+ }
257
+ seenIds.add(id4);
258
+ seenEmails.add(normalized);
259
+ grants.push({
260
+ id: id4,
261
+ email,
262
+ decision,
263
+ ...note ? { note } : {},
264
+ ...expiresAt !== void 0 ? { expiresAt } : {}
265
+ });
266
+ }
267
+ if (errors.length) return { ok: false, errors };
268
+ return {
269
+ ok: true,
270
+ value: {
271
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
272
+ revision,
273
+ generatedAt,
274
+ source: { appId: sourceId, actor: sourceActor },
275
+ target: { chapterId, environment },
276
+ grants
277
+ }
278
+ };
279
+ }
280
+ async function createAdmissionGrantEnvelope(document) {
281
+ const checked = validateAdmissionGrantDocument(document);
282
+ if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
283
+ return { document: checked.value, digest: await signupControlDigest(checked.value) };
284
+ }
285
+ var rows = (value) => Array.isArray(value) ? value : [];
286
+ function asGrant(row) {
287
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
288
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
289
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
290
+ return row;
291
+ }
292
+ async function findAdmissionGrant(db, targetId, email, now) {
293
+ const emailNormalized = normalizeAdmissionEmail(email);
294
+ if (!isAdmissionEmail(emailNormalized)) return null;
295
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
296
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
297
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
298
+ )).sort((a, b) => {
299
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
300
+ return scope || b.createdAt - a.createdAt;
301
+ });
302
+ return active[0] ?? null;
303
+ }
304
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
305
+ const where = { sourceId, targetId, environment };
306
+ if (db.aggregate) {
307
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
308
+ }
309
+ const found = await db.query({ admissionGrantRevisions: {} });
310
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
311
+ }
312
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
313
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
314
+ const result = await db.query({ admissionGrantRevisions: { $: {
315
+ where: { sourceId, targetId, environment },
316
+ order: { revision: "desc" },
317
+ limit: 1
318
+ } } });
319
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
320
+ return row ? row : null;
321
+ }
322
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
323
+ const { document, digest } = envelope;
324
+ const current = rows((await db.query({ admissionGrants: { $: {
325
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
326
+ limit: MAX_GRANTS
327
+ } } })).admissionGrants);
328
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
329
+ const ops = [];
330
+ for (const row of current) {
331
+ const id4 = String(row.id ?? "");
332
+ if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
333
+ active: false,
334
+ revokedAt: receivedAt,
335
+ revokedBy: document.source.actor,
336
+ updatedAt: receivedAt,
337
+ updatedBy: document.source.actor
338
+ } });
339
+ }
340
+ for (const grant of document.grants) {
341
+ const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
342
+ const existing = current.find((row) => row.id === id4);
343
+ ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
344
+ id: id4,
345
+ sourceGrantId: grant.id,
346
+ sourceScope: "parent",
347
+ sourceId: document.source.appId,
348
+ targetId: document.target.chapterId,
349
+ email: grant.email,
350
+ emailNormalized: normalizeAdmissionEmail(grant.email),
351
+ decision: grant.decision,
352
+ ...grant.note ? { note: grant.note } : {},
353
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
354
+ active: true,
355
+ sourceRevision: document.revision,
356
+ createdAt: Number(existing?.createdAt ?? receivedAt),
357
+ createdBy: String(existing?.createdBy ?? document.source.actor),
358
+ updatedAt: receivedAt,
359
+ updatedBy: document.source.actor
360
+ } });
361
+ const retract = [
362
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
363
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
364
+ ];
365
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
366
+ }
367
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
368
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
369
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
370
+ id: revisionId,
371
+ sourceId: document.source.appId,
372
+ targetId: document.target.chapterId,
373
+ environment: document.target.environment,
374
+ revision: document.revision,
375
+ digest,
376
+ active: true,
377
+ receivedAt
378
+ } });
379
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
380
+ }
381
+
155
382
  // src/member.ts
156
383
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
157
384
  var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
@@ -203,9 +430,9 @@ function resolveApplication(a) {
203
430
  validateEmail: a?.validateEmail ?? true
204
431
  };
205
432
  }
206
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
433
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
207
434
  function isValidEmail(value) {
208
- return typeof value === "string" && EMAIL_RE.test(value);
435
+ return typeof value === "string" && EMAIL_RE2.test(value);
209
436
  }
210
437
  function clampArray(value, max) {
211
438
  if (!Array.isArray(value)) return value;
@@ -247,14 +474,23 @@ async function submitApplication(db, chapter, fields, opts) {
247
474
  if (app.requireDisclaimerAck && !acked) {
248
475
  return { ok: false, error: "disclaimerAck is required" };
249
476
  }
250
- const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
251
- const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
477
+ const id4 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
478
+ const row = { id: id4, status: chapter.pipeline.initial, createdAt: opts.now };
252
479
  for (const f of [...app.required, ...app.optional]) {
253
480
  if (fieldStates[f]?.visible === false) continue;
254
481
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
255
482
  }
256
483
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
257
484
  if (opts.groupId) row.groupId = opts.groupId;
485
+ if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
486
+ row.emailNormalized = normalizeAdmissionEmail(fields.email);
487
+ }
488
+ if (opts.admissionGrant?.decision === "waive_interview") {
489
+ row.admissionGrantId = opts.admissionGrant.id;
490
+ row.admissionSource = opts.admissionGrant.sourceScope;
491
+ row.interviewWaivedAt = opts.now;
492
+ }
493
+ if (opts.giftOfferId) row.giftOfferId = opts.giftOfferId;
258
494
  if (typeof fields.tierId === "string" && fields.tierId) {
259
495
  if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
260
496
  return { ok: false, error: "tierId is not an offered tier" };
@@ -262,11 +498,20 @@ async function submitApplication(db, chapter, fields, opts) {
262
498
  row.tierId = fields.tierId;
263
499
  }
264
500
  if (acked) row.disclaimerAckAt = opts.now;
501
+ const ops = [
502
+ { t: "update", ns: "applications", id: id4, attrs: row }
503
+ ];
504
+ if (opts.admissionGrant?.decision === "waive_interview") {
505
+ ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
506
+ consumedAt: opts.now,
507
+ consumedApplicationId: id4
508
+ } });
509
+ }
265
510
  const { duplicate } = await db.transact(
266
- [{ t: "update", ns: "applications", id: id2, attrs: row }],
511
+ ops,
267
512
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
268
513
  );
269
- return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
514
+ return { ok: true, id: id4, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
270
515
  }
271
516
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
272
517
  return {
@@ -286,93 +531,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
286
531
  };
287
532
  }
288
533
 
289
- // src/schema.ts
290
- function attr(type, flags = {}) {
291
- return {
292
- type,
293
- unique: flags.unique ?? false,
294
- indexed: flags.indexed ?? false,
295
- optional: flags.optional ?? false
296
- };
297
- }
298
- var id = () => attr("string", { unique: true, indexed: true });
299
- var admins = {
300
- attrs: {
301
- id: id(),
302
- email: attr("string", { unique: true, indexed: true }),
303
- name: attr("string", { optional: true }),
304
- note: attr("string", { optional: true })
305
- }
306
- };
307
- var superAdmins = {
308
- attrs: {
309
- id: id(),
310
- email: attr("string", { unique: true, indexed: true }),
311
- note: attr("string", { optional: true }),
312
- createdAt: attr("number", { indexed: true })
313
- }
314
- };
315
- var networkNotes = {
316
- attrs: {
317
- id: id(),
318
- recordKey: attr("string", { indexed: true }),
319
- targetId: attr("string", { indexed: true }),
320
- recordType: attr("string", { indexed: true }),
321
- recordId: attr("string", { indexed: true }),
322
- body: attr("string"),
323
- authorId: attr("string", { indexed: true }),
324
- authorEmail: attr("string", { optional: true }),
325
- createdAt: attr("number", { indexed: true })
326
- }
327
- };
328
- var signupControls = {
329
- attrs: {
330
- id: id(),
331
- sourceId: attr("string", { indexed: true }),
332
- targetId: attr("string", { indexed: true }),
333
- environment: attr("string", { indexed: true }),
334
- revision: attr("number", { indexed: true }),
335
- digest: attr("string", { indexed: true }),
336
- document: attr("json"),
337
- active: attr("boolean", { indexed: true }),
338
- receivedAt: attr("number", { indexed: true })
339
- }
340
- };
341
- var applications = {
342
- attrs: {
343
- id: id(),
344
- firstName: attr("string"),
345
- lastName: attr("string"),
346
- email: attr("string", { indexed: true }),
347
- referral: attr("string"),
348
- referralName: attr("string", { optional: true }),
349
- whoYouAre: attr("string"),
350
- focus: attr("json"),
351
- linkedin: attr("string", { optional: true }),
352
- message: attr("string"),
353
- status: attr("string", { indexed: true }),
354
- createdAt: attr("number", { indexed: true }),
355
- meetingAt: attr("number", { indexed: true, optional: true }),
356
- meetingLink: attr("string", { optional: true }),
357
- clerkUserId: attr("string", { indexed: true, optional: true }),
358
- // One-time/effect-repair marker. Absent means /api/me should copy the
359
- // allowlisted application snapshot into Clerk private_metadata and remove
360
- // the former public_metadata copy.
361
- clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
362
- phone: attr("string", { optional: true }),
363
- state: attr("string", { optional: true }),
364
- groupId: attr("string", { indexed: true, optional: true }),
365
- // The tier this application joined on. Absent on rows that predate tiers.
366
- tierId: attr("string", { indexed: true, optional: true }),
367
- stripeCustomerId: attr("string", { indexed: true, optional: true }),
368
- stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
369
- renewalAt: attr("number", { optional: true }),
370
- disclaimerAckAt: attr("number", { optional: true }),
371
- refundPolicyAckAt: attr("number", { optional: true }),
372
- prepEmailSentAt: attr("number", { optional: true }),
373
- canceled: attr("boolean", { optional: true })
374
- }
375
- };
534
+ // src/schema-applications.ts
535
+ var attr = (type, flags = {}) => ({
536
+ type,
537
+ unique: flags.unique ?? false,
538
+ indexed: flags.indexed ?? false,
539
+ optional: flags.optional ?? false
540
+ });
376
541
  var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
377
542
  "firstName",
378
543
  "lastName",
@@ -386,25 +551,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
386
551
  "phone",
387
552
  "state"
388
553
  ]);
554
+ var applicationAttrs = {
555
+ id: attr("string", { unique: true, indexed: true }),
556
+ firstName: attr("string"),
557
+ lastName: attr("string"),
558
+ email: attr("string", { indexed: true }),
559
+ referral: attr("string"),
560
+ referralName: attr("string", { optional: true }),
561
+ whoYouAre: attr("string"),
562
+ focus: attr("json"),
563
+ linkedin: attr("string", { optional: true }),
564
+ message: attr("string"),
565
+ status: attr("string", { indexed: true }),
566
+ createdAt: attr("number", { indexed: true }),
567
+ meetingAt: attr("number", { indexed: true, optional: true }),
568
+ meetingLink: attr("string", { optional: true }),
569
+ clerkUserId: attr("string", { indexed: true, optional: true }),
570
+ clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
571
+ phone: attr("string", { optional: true }),
572
+ state: attr("string", { optional: true }),
573
+ groupId: attr("string", { indexed: true, optional: true }),
574
+ tierId: attr("string", { indexed: true, optional: true }),
575
+ stripeCustomerId: attr("string", { indexed: true, optional: true }),
576
+ stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
577
+ renewalAt: attr("number", { optional: true }),
578
+ disclaimerAckAt: attr("number", { optional: true }),
579
+ refundPolicyAckAt: attr("number", { optional: true }),
580
+ prepEmailSentAt: attr("number", { optional: true }),
581
+ canceled: attr("boolean", { optional: true }),
582
+ emailNormalized: attr("string", { indexed: true, optional: true }),
583
+ admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
584
+ admissionSource: attr("string", { indexed: true, optional: true }),
585
+ interviewWaivedAt: attr("number", { optional: true }),
586
+ giftOfferId: attr("string", { optional: true, indexed: true }),
587
+ giftIntentId: attr("string", { optional: true, unique: true, indexed: true }),
588
+ giftClaimId: attr("string", { optional: true, unique: true, indexed: true }),
589
+ giftOrderId: attr("string", { optional: true, indexed: true }),
590
+ giftPaidAt: attr("number", { optional: true, indexed: true }),
591
+ giftRevokedAt: attr("number", { optional: true, indexed: true }),
592
+ giftCheckoutStartedAt: attr("number", { optional: true, indexed: true })
593
+ };
389
594
  function applicationsFor(application) {
390
595
  const attrs = {};
391
- for (const [name, spec] of Object.entries(applications.attrs)) {
392
- attrs[name] = {
393
- ...spec,
394
- optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
395
- };
596
+ for (const [name, spec] of Object.entries(applicationAttrs)) {
597
+ attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
396
598
  }
397
599
  const apply = (field, optional) => {
398
600
  const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
399
- if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
400
- throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
401
- }
402
- if (existing && existing.type !== "string") {
403
- throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
404
- }
405
- const spec = existing ? { ...existing, optional } : attr("string", { optional });
601
+ if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
602
+ if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
406
603
  Object.defineProperty(attrs, field, {
407
- value: spec,
604
+ value: existing ? { ...existing, optional } : attr("string", { optional }),
408
605
  enumerable: true,
409
606
  configurable: true,
410
607
  writable: true
@@ -414,75 +611,270 @@ function applicationsFor(application) {
414
611
  for (const field of application.optional) apply(field, true);
415
612
  return { attrs };
416
613
  }
417
- var groups = {
614
+
615
+ // src/schema-admission.ts
616
+ var attr2 = (type, flags = {}) => ({
617
+ type,
618
+ unique: flags.unique ?? false,
619
+ indexed: flags.indexed ?? false,
620
+ optional: flags.optional ?? false
621
+ });
622
+ var id = () => attr2("string", { unique: true, indexed: true });
623
+ var admissionGrantsEntity = {
418
624
  attrs: {
419
625
  id: id(),
420
- name: attr("string"),
421
- standardPriceCents: attr("number"),
422
- foundingDiscountCents: attr("number"),
423
- stripePriceId: attr("string", { optional: true }),
424
- stripePublishableKey: attr("string", { optional: true }),
425
- notificationEmail: attr("string"),
426
- replyTo: attr("string"),
427
- debugEmail: attr("string", { optional: true }),
428
- calendarLink: attr("string", { optional: true }),
429
- disclaimerText: attr("string"),
430
- refundPolicyText: attr("string"),
431
- trustCopy: attr("string"),
432
- commitmentText: attr("string", { optional: true }),
433
- normsText: attr("string", { optional: true }),
434
- emailTemplates: attr("json"),
435
- schedulingJson: attr("json", { optional: true }),
436
- createdAt: attr("number", { indexed: true })
626
+ sourceGrantId: attr2("string", { indexed: true }),
627
+ sourceScope: attr2("string", { indexed: true }),
628
+ sourceId: attr2("string", { indexed: true }),
629
+ targetId: attr2("string", { indexed: true }),
630
+ email: attr2("string"),
631
+ emailNormalized: attr2("string", { indexed: true }),
632
+ decision: attr2("string", { indexed: true }),
633
+ note: attr2("string", { optional: true }),
634
+ active: attr2("boolean", { indexed: true }),
635
+ sourceRevision: attr2("number", { optional: true, indexed: true }),
636
+ createdAt: attr2("number", { indexed: true }),
637
+ createdBy: attr2("string", { optional: true, indexed: true }),
638
+ updatedAt: attr2("number", { optional: true, indexed: true }),
639
+ updatedBy: attr2("string", { optional: true, indexed: true }),
640
+ expiresAt: attr2("number", { optional: true, indexed: true }),
641
+ revokedAt: attr2("number", { optional: true, indexed: true }),
642
+ revokedBy: attr2("string", { optional: true, indexed: true }),
643
+ consumedAt: attr2("number", { optional: true, indexed: true }),
644
+ consumedApplicationId: attr2("string", { optional: true, indexed: true })
437
645
  }
438
646
  };
439
- var tiers = {
647
+ var admissionGrantRevisionsEntity = {
440
648
  attrs: {
441
649
  id: id(),
442
- groupId: attr("string", { indexed: true }),
443
- name: attr("string"),
444
- priceCents: attr("number"),
445
- stripePriceId: attr("string", { optional: true }),
446
- blurb: attr("string", { optional: true }),
447
- sortOrder: attr("number", { indexed: true }),
448
- active: attr("boolean", { indexed: true }),
449
- createdAt: attr("number", { indexed: true })
650
+ sourceId: attr2("string", { indexed: true }),
651
+ targetId: attr2("string", { indexed: true }),
652
+ environment: attr2("string", { indexed: true }),
653
+ revision: attr2("number", { indexed: true }),
654
+ digest: attr2("string", { indexed: true }),
655
+ active: attr2("boolean", { indexed: true }),
656
+ receivedAt: attr2("number", { indexed: true })
657
+ }
658
+ };
659
+
660
+ // src/schema-gifts.ts
661
+ var attr3 = (type, flags = {}) => ({
662
+ type,
663
+ unique: flags.unique ?? false,
664
+ indexed: flags.indexed ?? false,
665
+ optional: flags.optional ?? false
666
+ });
667
+ var id2 = () => attr3("string", { unique: true, indexed: true });
668
+ var giftPurchaseIntentsEntity = { attrs: {
669
+ id: id2(),
670
+ chapterId: attr3("string", { indexed: true }),
671
+ environment: attr3("string", { indexed: true }),
672
+ purchaserType: attr3("string", { indexed: true }),
673
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
674
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
675
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
676
+ offerId: attr3("string", { indexed: true }),
677
+ offerSnapshot: attr3("json"),
678
+ status: attr3("string", { indexed: true }),
679
+ codeHash: attr3("string", { indexed: true }),
680
+ codeVersion: attr3("number"),
681
+ codeIssuedAt: attr3("number"),
682
+ providerObjectId: attr3("string", { optional: true, unique: true, indexed: true }),
683
+ providerCustomerId: attr3("string", { optional: true, indexed: true }),
684
+ checkoutStartedAt: attr3("number", { optional: true }),
685
+ paidAt: attr3("number", { optional: true, indexed: true }),
686
+ claimId: attr3("string", { optional: true, unique: true, indexed: true }),
687
+ createdAt: attr3("number", { indexed: true }),
688
+ updatedAt: attr3("number", { indexed: true })
689
+ } };
690
+ var giftOrdersEntity = { attrs: {
691
+ id: id2(),
692
+ intentId: attr3("string", { unique: true, indexed: true }),
693
+ chapterId: attr3("string", { indexed: true }),
694
+ environment: attr3("string", { indexed: true }),
695
+ offerId: attr3("string", { indexed: true }),
696
+ offerSnapshot: attr3("json"),
697
+ purchaserType: attr3("string", { indexed: true }),
698
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
699
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
700
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
701
+ providerEventId: attr3("string", { unique: true, indexed: true }),
702
+ providerObjectId: attr3("string", { indexed: true }),
703
+ amountCents: attr3("number"),
704
+ currency: attr3("string"),
705
+ billingMode: attr3("string"),
706
+ createdAt: attr3("number", { indexed: true })
707
+ } };
708
+ var giftClaimsEntity = { attrs: {
709
+ id: id2(),
710
+ orderId: attr3("string", { unique: true, indexed: true }),
711
+ intentId: attr3("string", { unique: true, indexed: true }),
712
+ chapterId: attr3("string", { indexed: true }),
713
+ offerId: attr3("string", { indexed: true }),
714
+ target: attr3("json"),
715
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
716
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
717
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
718
+ codeHash: attr3("string", { unique: true, indexed: true }),
719
+ codeVersion: attr3("number"),
720
+ rotationCount: attr3("number"),
721
+ codeIssuedAt: attr3("number"),
722
+ codeExpiresAt: attr3("number", { indexed: true }),
723
+ attemptCount: attr3("number"),
724
+ attemptWindowStartedAt: attr3("number"),
725
+ status: attr3("string", { indexed: true }),
726
+ redeemedAt: attr3("number", { optional: true, indexed: true }),
727
+ beneficiaryEmailNormalized: attr3("string", { optional: true, indexed: true }),
728
+ beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
729
+ applicationId: attr3("string", { optional: true, unique: true, indexed: true }),
730
+ entitlementId: attr3("string", { optional: true, unique: true, indexed: true }),
731
+ rotatedAt: attr3("number", { optional: true }),
732
+ revokedAt: attr3("number", { optional: true, indexed: true }),
733
+ revokedReason: attr3("string", { optional: true }),
734
+ createdAt: attr3("number", { indexed: true })
735
+ } };
736
+ var giftEntitlementsEntity = { attrs: {
737
+ id: id2(),
738
+ claimId: attr3("string", { unique: true, indexed: true }),
739
+ orderId: attr3("string", { indexed: true }),
740
+ chapterId: attr3("string", { indexed: true }),
741
+ entitlementKey: attr3("string", { indexed: true }),
742
+ beneficiaryEmailNormalized: attr3("string", { indexed: true }),
743
+ beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
744
+ active: attr3("boolean", { indexed: true }),
745
+ createdAt: attr3("number", { indexed: true }),
746
+ revokedAt: attr3("number", { optional: true })
747
+ } };
748
+ var giftRedemptionAttemptsEntity = { attrs: {
749
+ id: id2(),
750
+ chapterId: attr3("string", { indexed: true }),
751
+ bucket: attr3("string", { indexed: true }),
752
+ attemptCount: attr3("number"),
753
+ windowStartedAt: attr3("number", { indexed: true }),
754
+ updatedAt: attr3("number")
755
+ } };
756
+
757
+ // src/schema.ts
758
+ function attr4(type, flags = {}) {
759
+ return {
760
+ type,
761
+ unique: flags.unique ?? false,
762
+ indexed: flags.indexed ?? false,
763
+ optional: flags.optional ?? false
764
+ };
765
+ }
766
+ var id3 = () => attr4("string", { unique: true, indexed: true });
767
+ var admins = {
768
+ attrs: {
769
+ id: id3(),
770
+ email: attr4("string", { unique: true, indexed: true }),
771
+ name: attr4("string", { optional: true }),
772
+ note: attr4("string", { optional: true })
773
+ }
774
+ };
775
+ var superAdmins = {
776
+ attrs: {
777
+ id: id3(),
778
+ email: attr4("string", { unique: true, indexed: true }),
779
+ note: attr4("string", { optional: true }),
780
+ createdAt: attr4("number", { indexed: true })
781
+ }
782
+ };
783
+ var networkNotes = {
784
+ attrs: {
785
+ id: id3(),
786
+ recordKey: attr4("string", { indexed: true }),
787
+ targetId: attr4("string", { indexed: true }),
788
+ recordType: attr4("string", { indexed: true }),
789
+ recordId: attr4("string", { indexed: true }),
790
+ body: attr4("string"),
791
+ authorId: attr4("string", { indexed: true }),
792
+ authorEmail: attr4("string", { optional: true }),
793
+ createdAt: attr4("number", { indexed: true })
794
+ }
795
+ };
796
+ var signupControls = {
797
+ attrs: {
798
+ id: id3(),
799
+ sourceId: attr4("string", { indexed: true }),
800
+ targetId: attr4("string", { indexed: true }),
801
+ environment: attr4("string", { indexed: true }),
802
+ revision: attr4("number", { indexed: true }),
803
+ digest: attr4("string", { indexed: true }),
804
+ document: attr4("json"),
805
+ active: attr4("boolean", { indexed: true }),
806
+ receivedAt: attr4("number", { indexed: true })
807
+ }
808
+ };
809
+ var groups = {
810
+ attrs: {
811
+ id: id3(),
812
+ name: attr4("string"),
813
+ standardPriceCents: attr4("number"),
814
+ foundingDiscountCents: attr4("number"),
815
+ stripePriceId: attr4("string", { optional: true }),
816
+ stripePublishableKey: attr4("string", { optional: true }),
817
+ notificationEmail: attr4("string"),
818
+ replyTo: attr4("string"),
819
+ debugEmail: attr4("string", { optional: true }),
820
+ calendarLink: attr4("string", { optional: true }),
821
+ disclaimerText: attr4("string"),
822
+ refundPolicyText: attr4("string"),
823
+ trustCopy: attr4("string"),
824
+ commitmentText: attr4("string", { optional: true }),
825
+ normsText: attr4("string", { optional: true }),
826
+ emailTemplates: attr4("json"),
827
+ schedulingJson: attr4("json", { optional: true }),
828
+ createdAt: attr4("number", { indexed: true })
829
+ }
830
+ };
831
+ var tiers = {
832
+ attrs: {
833
+ id: id3(),
834
+ groupId: attr4("string", { indexed: true }),
835
+ name: attr4("string"),
836
+ priceCents: attr4("number"),
837
+ stripePriceId: attr4("string", { optional: true }),
838
+ blurb: attr4("string", { optional: true }),
839
+ sortOrder: attr4("number", { indexed: true }),
840
+ active: attr4("boolean", { indexed: true }),
841
+ createdAt: attr4("number", { indexed: true })
450
842
  }
451
843
  };
452
844
  var meetings = {
453
845
  attrs: {
454
- id: id(),
455
- applicationId: attr("string", { indexed: true }),
456
- groupId: attr("string", { indexed: true }),
457
- startAt: attr("number", { indexed: true }),
458
- endAt: attr("number"),
459
- timezone: attr("string"),
460
- status: attr("string", { indexed: true }),
461
- googleEventId: attr("string", { indexed: true, optional: true }),
462
- meetUrl: attr("string", { optional: true }),
463
- htmlLink: attr("string", { optional: true }),
464
- drift: attr("string", { indexed: true, optional: true }),
465
- driftGoogleStartAt: attr("number", { optional: true }),
466
- driftDetectedAt: attr("number", { optional: true }),
467
- adoptedFromGoogleAt: attr("number", { optional: true }),
468
- createdAt: attr("number", { indexed: true })
846
+ id: id3(),
847
+ applicationId: attr4("string", { indexed: true }),
848
+ groupId: attr4("string", { indexed: true }),
849
+ startAt: attr4("number", { indexed: true }),
850
+ endAt: attr4("number"),
851
+ timezone: attr4("string"),
852
+ status: attr4("string", { indexed: true }),
853
+ googleEventId: attr4("string", { indexed: true, optional: true }),
854
+ meetUrl: attr4("string", { optional: true }),
855
+ htmlLink: attr4("string", { optional: true }),
856
+ drift: attr4("string", { indexed: true, optional: true }),
857
+ driftGoogleStartAt: attr4("number", { optional: true }),
858
+ driftDetectedAt: attr4("number", { optional: true }),
859
+ adoptedFromGoogleAt: attr4("number", { optional: true }),
860
+ createdAt: attr4("number", { indexed: true })
469
861
  }
470
862
  };
471
863
  var emailLog = {
472
864
  attrs: {
473
- id: id(),
474
- groupId: attr("string", { indexed: true }),
475
- applicationId: attr("string", { indexed: true, optional: true }),
476
- to: attr("string", { indexed: true }),
477
- template: attr("string", { indexed: true }),
478
- subject: attr("string"),
479
- body: attr("string", { optional: true }),
480
- transport: attr("string"),
481
- messageId: attr("string", { optional: true }),
482
- redirected: attr("boolean", { optional: true }),
483
- dedupeKey: attr("string", { indexed: true, optional: true }),
484
- error: attr("string", { optional: true }),
485
- sentAt: attr("number", { indexed: true })
865
+ id: id3(),
866
+ groupId: attr4("string", { indexed: true }),
867
+ applicationId: attr4("string", { indexed: true, optional: true }),
868
+ to: attr4("string", { indexed: true }),
869
+ template: attr4("string", { indexed: true }),
870
+ subject: attr4("string"),
871
+ body: attr4("string", { optional: true }),
872
+ transport: attr4("string"),
873
+ messageId: attr4("string", { optional: true }),
874
+ redirected: attr4("boolean", { optional: true }),
875
+ dedupeKey: attr4("string", { indexed: true, optional: true }),
876
+ error: attr4("string", { optional: true }),
877
+ sentAt: attr4("number", { indexed: true })
486
878
  }
487
879
  };
488
880
  function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
@@ -493,6 +885,13 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
493
885
  entities.tiers = tiers;
494
886
  entities.meetings = meetings;
495
887
  entities.emailLog = emailLog;
888
+ entities.admissionGrants = admissionGrantsEntity;
889
+ entities.admissionGrantRevisions = admissionGrantRevisionsEntity;
890
+ entities.giftPurchaseIntents = giftPurchaseIntentsEntity;
891
+ entities.giftOrders = giftOrdersEntity;
892
+ entities.giftClaims = giftClaimsEntity;
893
+ entities.giftEntitlements = giftEntitlementsEntity;
894
+ entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
496
895
  if (includeSignupControl) entities.signupControls = signupControls;
497
896
  }
498
897
  if (auth.source === "table") entities.admins = admins;
@@ -777,6 +1176,7 @@ var DEFAULT_ADMIN_COPY = {
777
1176
  overview: "Overview",
778
1177
  billing: "Billing",
779
1178
  people: "People",
1179
+ admission: "Admission",
780
1180
  network: "Network",
781
1181
  portfolio: "Portfolio",
782
1182
  settings: "Settings",
@@ -887,6 +1287,26 @@ var DEFAULT_ADMIN_COPY = {
887
1287
  reschedule: "Reschedule",
888
1288
  cancel: "Cancel"
889
1289
  },
1290
+ admission: {
1291
+ title: "Interview decisions",
1292
+ description: "Local decisions override Parent decisions. Requiring an interview locally suppresses a Parent waiver for the same email.",
1293
+ email: "Email",
1294
+ decision: "Decision",
1295
+ expires: "Expires",
1296
+ note: "Internal note",
1297
+ waive: "Waive interview",
1298
+ require: "Require interview",
1299
+ add: "Add decision",
1300
+ ledger: "Effective grant ledger",
1301
+ loading: "Loading\u2026",
1302
+ consumed: "consumed",
1303
+ revoked: "revoked",
1304
+ expired: "expired",
1305
+ active: "active",
1306
+ local: "local",
1307
+ parent: "Parent",
1308
+ revoke: "Revoke"
1309
+ },
890
1310
  network: {
891
1311
  title: "Share to a follower",
892
1312
  allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
@@ -985,6 +1405,19 @@ var DEFAULT_JOIN_COPY = {
985
1405
  label: "You're booked",
986
1406
  calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
987
1407
  memberArea: "Go to your member area"
1408
+ },
1409
+ accepted: {
1410
+ label: "You're approved",
1411
+ body: "Your membership is approved. No introduction call is required."
1412
+ },
1413
+ gift: {
1414
+ chooseSelf: "Buy membership for myself",
1415
+ optionsLabel: "Or give access to someone else",
1416
+ label: "Your gift code",
1417
+ body: "Share this one-time code with the recipient. It becomes active after payment is confirmed.",
1418
+ save: "Save it now. For security, this code is shown only once.",
1419
+ rotate: "Generate a new code",
1420
+ rotateFailed: "A new code could not be generated."
988
1421
  }
989
1422
  };
990
1423
  var DEFAULT_MEMBERS_COPY = {
@@ -1012,6 +1445,26 @@ var DEFAULT_MEMBERS_COPY = {
1012
1445
  full: {
1013
1446
  welcome: "Welcome back."
1014
1447
  },
1448
+ gifts: {
1449
+ title: "Give access",
1450
+ body: "Purchase access for someone else and share the one-time claim code.",
1451
+ noneAvailable: "No gift options are available right now.",
1452
+ paid: "Payment submitted. Save the code below while it is visible.",
1453
+ saveCode: "This code is shown once. Save it before leaving this page.",
1454
+ purchases: "Your gifts",
1455
+ noPurchases: "No gift purchases yet.",
1456
+ pending: "Pending",
1457
+ redeemed: "Redeemed",
1458
+ expired: "Expired",
1459
+ revoked: "Revoked",
1460
+ rotate: "Rotate code",
1461
+ rotated: "A new code was generated. Save it now.",
1462
+ rotateFailed: "The code could not be rotated.",
1463
+ redeemTitle: "Redeem a gift",
1464
+ code: "One-time code",
1465
+ redeem: "Redeem",
1466
+ redeemFailed: "The code is invalid or unavailable."
1467
+ },
1015
1468
  reschedule: {
1016
1469
  changeTime: "Change your time",
1017
1470
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
@@ -1127,13 +1580,13 @@ function resolveLeaderFormation(input, crm) {
1127
1580
  }
1128
1581
  function formationFields(crm, formation) {
1129
1582
  const def = crm.type(formation.type);
1130
- return [...formation.required, ...formation.optional].map((id2) => {
1131
- const field = def.fields[id2];
1583
+ return [...formation.required, ...formation.optional].map((id4) => {
1584
+ const field = def.fields[id4];
1132
1585
  return {
1133
- id: id2,
1134
- label: field.label ?? id2,
1586
+ id: id4,
1587
+ label: field.label ?? id4,
1135
1588
  type: field.type,
1136
- required: formation.required.includes(id2),
1589
+ required: formation.required.includes(id4),
1137
1590
  ...field.options ? { options: field.options } : {}
1138
1591
  };
1139
1592
  });
@@ -1292,9 +1745,9 @@ function isResolvedCrm(x) {
1292
1745
  }
1293
1746
  function defineChapter(config) {
1294
1747
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
1295
- const { id: id2, name } = config;
1296
- if (typeof id2 !== "string" || !SLUG3.test(id2)) {
1297
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
1748
+ const { id: id4, name } = config;
1749
+ if (typeof id4 !== "string" || !SLUG3.test(id4)) {
1750
+ throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
1298
1751
  }
1299
1752
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
1300
1753
  const mode = config.mode ?? "chapter";
@@ -1377,7 +1830,7 @@ function defineChapter(config) {
1377
1830
  };
1378
1831
  const chapter = {
1379
1832
  config,
1380
- id: id2,
1833
+ id: id4,
1381
1834
  name,
1382
1835
  mode,
1383
1836
  brand,
@@ -1591,8 +2044,8 @@ function tierPayable(tier, group, hasSecretKey) {
1591
2044
  function byOrder(a, b) {
1592
2045
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1593
2046
  }
1594
- function resolveTiers(group, rows = []) {
1595
- const stored = rows.filter((tier) => tier.active);
2047
+ function resolveTiers(group, rows2 = []) {
2048
+ const stored = rows2.filter((tier) => tier.active);
1596
2049
  if (stored.length) return [...stored].sort(byOrder);
1597
2050
  return [{
1598
2051
  id: "standard",
@@ -1714,10 +2167,10 @@ function planDelivery(input) {
1714
2167
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1715
2168
  const to = redirect ? input.group.debugEmail : input.to;
1716
2169
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1717
- const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
2170
+ const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
1718
2171
 
1719
2172
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1720
- return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
2173
+ return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
1721
2174
  }
1722
2175
 
1723
2176
  // src/notify.ts
@@ -1752,9 +2205,9 @@ async function sendTemplated(deps, input) {
1752
2205
  error = e instanceof Error ? e.message : String(e);
1753
2206
  }
1754
2207
  }
1755
- const id2 = deps.newId();
2208
+ const id4 = deps.newId();
1756
2209
  const row = {
1757
- id: id2,
2210
+ id: id4,
1758
2211
  groupId: input.group.id,
1759
2212
  to: decision.to,
1760
2213
  template: input.template,
@@ -1768,7 +2221,7 @@ async function sendTemplated(deps, input) {
1768
2221
  ...messageId ? { messageId } : {},
1769
2222
  ...error ? { error } : {}
1770
2223
  };
1771
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id2, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
2224
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id4, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
1772
2225
  return error ? { sent: false, reason: error } : { sent: true };
1773
2226
  }
1774
2227
  function emailGroupFrom(row) {
@@ -1804,8 +2257,26 @@ function findApplicationRef(obj) {
1804
2257
  const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
1805
2258
  return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
1806
2259
  }
2260
+ function findGiftRef(obj) {
2261
+ const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
2262
+ const pick = (metadata) => typeof metadata.giftIntentId === "string" ? metadata.giftIntentId : void 0;
2263
+ const parent = obj.parent;
2264
+ const subscription = obj.subscription_details ?? parent?.subscription_details;
2265
+ const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
2266
+ const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
2267
+ return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
2268
+ }
1807
2269
  function normalizeWebhookEvent(event) {
1808
2270
  const obj = event.data?.object ?? {};
2271
+ const gift = findGiftRef(obj);
2272
+ if (gift.giftIntentId) {
2273
+ if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
2274
+ return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
2275
+ }
2276
+ if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
2277
+ if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
2278
+ return { kind: "ignored", type: event.type };
2279
+ }
1809
2280
  const ref = findApplicationRef(obj);
1810
2281
  switch (event.type) {
1811
2282
  case "invoice.paid": {
@@ -1839,6 +2310,112 @@ function canceledPatch() {
1839
2310
  return { canceled: true };
1840
2311
  }
1841
2312
 
2313
+ // src/join-continuation.ts
2314
+ function resolveJoinContinuation(input) {
2315
+ if (input.status === "approved") return "accepted";
2316
+ if (input.interviewWaived) {
2317
+ if (!input.payable) return "approve";
2318
+ if (input.status === input.initialStatus) {
2319
+ return input.subscriptionStarted ? "paymentPending" : "payment";
2320
+ }
2321
+ return "approve";
2322
+ }
2323
+ if (!input.payable) return "booking";
2324
+ if (input.status === input.initialStatus) {
2325
+ return input.subscriptionStarted ? "paymentPending" : "payment";
2326
+ }
2327
+ if (canBook(input.status, input.pipeline)) return "booking";
2328
+ return null;
2329
+ }
2330
+
2331
+ // src/gift-offers.ts
2332
+ var GIFT_OFFER_CATALOG_VERSION = 1;
2333
+ var MAX_GIFT_OFFERS = 20;
2334
+ var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
2335
+ var PRICE = /^price_[A-Za-z0-9]{12,}$/;
2336
+ var CURRENCY = /^[a-z]{3}$/;
2337
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
2338
+ var text = (value, path, errors, max) => {
2339
+ if (typeof value !== "string" || !value.trim()) {
2340
+ errors.push(`${path} is required`);
2341
+ return "";
2342
+ }
2343
+ if (value.trim().length > max) errors.push(`${path} must be at most ${max} characters`);
2344
+ return value.trim();
2345
+ };
2346
+ function validateGiftOfferCatalog(input, tierIds) {
2347
+ const errors = [];
2348
+ const root = record(input);
2349
+ if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
2350
+ for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
2351
+ if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
2352
+ if (!Array.isArray(root.offers)) errors.push("document.giftOffers.offers must be an array");
2353
+ const rawOffers = Array.isArray(root.offers) ? root.offers : [];
2354
+ if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
2355
+ const ids = /* @__PURE__ */ new Set();
2356
+ const offers = [];
2357
+ for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
2358
+ const path = `document.giftOffers.offers[${index}]`;
2359
+ const item = record(raw);
2360
+ if (!item) {
2361
+ errors.push(`${path} must be an object`);
2362
+ continue;
2363
+ }
2364
+ const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
2365
+ for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2366
+ const id4 = text(item.id, `${path}.id`, errors, 80);
2367
+ if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2368
+ if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
2369
+ ids.add(id4);
2370
+ const targetRaw = record(item.target);
2371
+ let target = { type: "entitlement", entitlementKey: "invalid" };
2372
+ if (targetRaw?.type === "tier") {
2373
+ const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
2374
+ if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
2375
+ target = { type: "tier", tierId };
2376
+ } else if (targetRaw?.type === "entitlement") {
2377
+ const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
2378
+ if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
2379
+ target = { type: "entitlement", entitlementKey };
2380
+ } else errors.push(`${path}.target.type must be tier or entitlement`);
2381
+ if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
2382
+ const stripePriceId = text(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
2383
+ if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2384
+ 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`);
2385
+ const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
2386
+ if (!CURRENCY.test(currency)) errors.push(`${path}.currency must be a lowercase ISO currency code`);
2387
+ const lifetime = Number(item.redemptionLifetimeHours);
2388
+ if (!Number.isSafeInteger(lifetime) || lifetime < 1 || lifetime > 8760) errors.push(`${path}.redemptionLifetimeHours must be an integer from 1 to 8760`);
2389
+ const activeFrom = item.activeFrom === void 0 ? void 0 : Number(item.activeFrom);
2390
+ const activeUntil = item.activeUntil === void 0 ? void 0 : Number(item.activeUntil);
2391
+ if (activeFrom !== void 0 && (!Number.isSafeInteger(activeFrom) || activeFrom < 1)) errors.push(`${path}.activeFrom must be epoch milliseconds`);
2392
+ if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
2393
+ if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
2394
+ offers.push({
2395
+ id: id4,
2396
+ target,
2397
+ billingMode: item.billingMode,
2398
+ stripePriceId,
2399
+ priceCents: Number(item.priceCents),
2400
+ currency,
2401
+ redemptionLifetimeHours: lifetime,
2402
+ ...activeFrom !== void 0 ? { activeFrom } : {},
2403
+ ...activeUntil !== void 0 ? { activeUntil } : {},
2404
+ title: text(item.title, `${path}.title`, errors, 160),
2405
+ description: text(item.description, `${path}.description`, errors, 2e3),
2406
+ ctaLabel: text(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
2407
+ });
2408
+ }
2409
+ return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
2410
+ }
2411
+ var giftOfferActive = (offer, now) => (!offer.activeFrom || offer.activeFrom <= now) && (!offer.activeUntil || offer.activeUntil > now);
2412
+ function publicGiftOffers(catalog, now) {
2413
+ return (catalog?.offers ?? []).filter((offer) => giftOfferActive(offer, now)).map(({ stripePriceId: _stripe, target: _target, ...offer }) => offer);
2414
+ }
2415
+ function findGiftOffer(catalog, offerId, now) {
2416
+ return catalog?.offers.find((offer) => offer.id === offerId && giftOfferActive(offer, now)) ?? null;
2417
+ }
2418
+
1842
2419
  // src/network.ts
1843
2420
  var import_crm5 = require("@odla-ai/crm");
1844
2421
 
@@ -1878,34 +2455,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
1878
2455
  if (readable && raw.length <= 64) return raw;
1879
2456
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
1880
2457
  }
1881
- function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1882
- if ("input" in record && record.version === 2) {
2458
+ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
2459
+ if ("input" in record2 && record2.version === 2) {
1883
2460
  return {
1884
2461
  version: 2,
1885
- sourceId: record.source.siteId,
1886
- sourceRecordId: record.source.recordId,
1887
- type: record.type,
1888
- input: record.input
2462
+ sourceId: record2.source.siteId,
2463
+ sourceRecordId: record2.source.recordId,
2464
+ type: record2.type,
2465
+ input: record2.input
1889
2466
  };
1890
2467
  }
1891
- if ("input" in record) {
2468
+ if ("input" in record2) {
1892
2469
  return {
1893
2470
  version: 1,
1894
2471
  sourceId: fallbackSourceId,
1895
- sourceRecordId: record.hubRecordId,
1896
- type: record.type,
1897
- input: record.input
2472
+ sourceRecordId: record2.hubRecordId,
2473
+ type: record2.type,
2474
+ input: record2.input
1898
2475
  };
1899
2476
  }
1900
- if ("type" in record && record.type === "company") {
1901
- const input = { name: record.name };
2477
+ if ("type" in record2 && record2.type === "company") {
2478
+ const input = { name: record2.name };
1902
2479
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
1903
- if (record[key]) input[key] = record[key];
2480
+ if (record2[key]) input[key] = record2[key];
1904
2481
  }
1905
2482
  return {
1906
2483
  version: 1,
1907
2484
  sourceId: fallbackSourceId,
1908
- sourceRecordId: record.hubRecordId,
2485
+ sourceRecordId: record2.hubRecordId,
1909
2486
  type: "company",
1910
2487
  input
1911
2488
  };
@@ -1913,28 +2490,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1913
2490
  return {
1914
2491
  version: 1,
1915
2492
  sourceId: fallbackSourceId,
1916
- sourceRecordId: record.hubRecordId,
2493
+ sourceRecordId: record2.hubRecordId,
1917
2494
  type: "person",
1918
- input: sharedPersonInput(record)
2495
+ input: sharedPersonInput(record2)
1919
2496
  };
1920
2497
  }
1921
- function sharedRecordFromCrm(crm, record, target, sourceId) {
1922
- if (target.fields && !target.fields[record.type]) {
1923
- throw new Error(`${target.name} does not accept "${record.type}" records`);
2498
+ function sharedRecordFromCrm(crm, record2, target, sourceId) {
2499
+ if (target.fields && !target.fields[record2.type]) {
2500
+ throw new Error(`${target.name} does not accept "${record2.type}" records`);
1924
2501
  }
1925
- const def = crm.type(record.type);
1926
- const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
2502
+ const def = crm.type(record2.type);
2503
+ const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
1927
2504
  if (!fields) {
1928
- throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
2505
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record2.type}" records`);
1929
2506
  }
1930
2507
  const nameField = def.nameField ?? "name";
1931
2508
  const input = {};
1932
2509
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
1933
- const value = record.fields?.[field];
2510
+ const value = record2.fields?.[field];
1934
2511
  if (value !== void 0) input[field] = value;
1935
2512
  }
1936
- if (input[nameField] === void 0) input[nameField] = record.name;
1937
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
2513
+ if (input[nameField] === void 0) input[nameField] = record2.name;
2514
+ return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
1938
2515
  }
1939
2516
  async function upsertPerson(deps, opts) {
1940
2517
  const email = opts.email.toLowerCase();
@@ -1948,11 +2525,11 @@ async function upsertPerson(deps, opts) {
1948
2525
  const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
1949
2526
  return { recordId: created.id };
1950
2527
  }
1951
- async function findSharedRecord(deps, record, tag) {
2528
+ async function findSharedRecord(deps, record2, tag) {
1952
2529
  const structured = await (0, import_crm5.getRecordByOrigin)(
1953
2530
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
1954
- record.sourceId,
1955
- record.sourceRecordId
2531
+ record2.sourceId,
2532
+ record2.sourceRecordId
1956
2533
  );
1957
2534
  if (structured) return structured;
1958
2535
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -1961,53 +2538,53 @@ async function findSharedRecord(deps, record, tag) {
1961
2538
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1962
2539
  if (found.crm_record?.[0]) return found.crm_record[0];
1963
2540
  }
1964
- const def = deps.crm.type(record.type);
2541
+ const def = deps.crm.type(record2.type);
1965
2542
  const emailField = def.emailField;
1966
- if (emailField && typeof record.input[emailField] === "string") {
1967
- const primaryEmail = record.input[emailField].toLowerCase();
2543
+ if (emailField && typeof record2.input[emailField] === "string") {
2544
+ const primaryEmail = record2.input[emailField].toLowerCase();
1968
2545
  const found = await deps.db.query({
1969
- crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
2546
+ crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
1970
2547
  });
1971
2548
  if (found.crm_record?.[0]) return found.crm_record[0];
1972
2549
  }
1973
- const domain = record.input.domain;
2550
+ const domain = record2.input.domain;
1974
2551
  const domainSlot = def.fields.domain?.slot;
1975
2552
  if (typeof domain === "string" && domainSlot) {
1976
2553
  const found = await deps.db.query({
1977
- crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
2554
+ crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
1978
2555
  });
1979
2556
  if (found.crm_record?.[0]) return found.crm_record[0];
1980
2557
  }
1981
2558
  const nameField = def.nameField ?? "name";
1982
- const name = record.input[nameField];
1983
- if (record.type === "company" && typeof name === "string" && name.trim()) {
2559
+ const name = record2.input[nameField];
2560
+ if (record2.type === "company" && typeof name === "string" && name.trim()) {
1984
2561
  const found = await deps.db.query({
1985
- crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
2562
+ crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
1986
2563
  });
1987
2564
  if (found.crm_record?.[0]) return found.crm_record[0];
1988
2565
  }
1989
2566
  return void 0;
1990
2567
  }
1991
2568
  async function projectSharedRecord(deps, shared, options = {}) {
1992
- const record = normalizeSharedRecord(shared, options.sourceId);
1993
- if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
2569
+ const record2 = normalizeSharedRecord(shared, options.sourceId);
2570
+ if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
1994
2571
  throw new Error("type, source site id, and source record id must be non-empty");
1995
2572
  }
1996
- if (options.sourceId && options.sourceId !== record.sourceId) {
2573
+ if (options.sourceId && options.sourceId !== record2.sourceId) {
1997
2574
  throw new Error("signed sender does not match payload source");
1998
2575
  }
1999
- const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
2576
+ const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
2000
2577
  const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
2001
- const existing = await findSharedRecord(deps, record, tag);
2578
+ const existing = await findSharedRecord(deps, record2, tag);
2002
2579
  let recordId;
2003
2580
  if (existing && typeof existing.id === "string") {
2004
- await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record.input });
2581
+ await (0, import_crm5.updateRecord)(crmDeps, { id: existing.id, input: record2.input });
2005
2582
  recordId = existing.id;
2006
2583
  } else {
2007
- recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
2584
+ recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
2008
2585
  await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
2009
- type: record.type,
2010
- input: record.input,
2586
+ type: record2.type,
2587
+ input: record2.input,
2011
2588
  mutationId: `share-create:${tag}`
2012
2589
  });
2013
2590
  }
@@ -2017,10 +2594,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
2017
2594
  );
2018
2595
  await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
2019
2596
  recordId,
2020
- sourceId: record.sourceId,
2021
- sourceRecordId: record.sourceRecordId,
2597
+ sourceId: record2.sourceId,
2598
+ sourceRecordId: record2.sourceRecordId,
2022
2599
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
2023
- payloadVersion: record.version
2600
+ payloadVersion: record2.version
2024
2601
  });
2025
2602
  return { recordId };
2026
2603
  }
@@ -2236,9 +2813,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
2236
2813
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
2237
2814
  if (!found.ok) return false;
2238
2815
  const users = await found.json().catch(() => null);
2239
- const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
2240
- if (!id2) return false;
2241
- return updateClerkUserMetadata(secretKey, id2, input, fetchImpl);
2816
+ const id4 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
2817
+ if (!id4) return false;
2818
+ return updateClerkUserMetadata(secretKey, id4, input, fetchImpl);
2242
2819
  }
2243
2820
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
2244
2821
  const { path, body } = clerkUserRequest(input);
@@ -2294,8 +2871,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
2294
2871
  const user = Array.isArray(data) ? data[0] : void 0;
2295
2872
  return user ? toRecord(user) : null;
2296
2873
  }
2297
- async function clerkGetUser(secretKey, id2, fetchImpl = fetch) {
2298
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id2)}`, secretKey, fetchImpl).catch(() => null);
2874
+ async function clerkGetUser(secretKey, id4, fetchImpl = fetch) {
2875
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id4)}`, secretKey, fetchImpl).catch(() => null);
2299
2876
  return data ? toRecord(data) : null;
2300
2877
  }
2301
2878
  async function clerkListUsers(secretKey, fetchImpl = fetch) {
@@ -2304,15 +2881,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
2304
2881
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
2305
2882
  const page = Array.isArray(data) ? data : [];
2306
2883
  for (const u of page) {
2307
- const record = toRecord(u);
2308
- if (record) out.push(record);
2884
+ const record2 = toRecord(u);
2885
+ if (record2) out.push(record2);
2309
2886
  }
2310
2887
  if (page.length < PAGE) break;
2311
2888
  }
2312
2889
  return out;
2313
2890
  }
2314
- async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
2315
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id2)}/metadata`, {
2891
+ async function clerkSetRole(secretKey, id4, role, fetchImpl = fetch) {
2892
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id4)}/metadata`, {
2316
2893
  method: "PATCH",
2317
2894
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
2318
2895
  body: JSON.stringify({ public_metadata: { role } })
@@ -2688,15 +3265,15 @@ function rowToTier(row, groupId) {
2688
3265
  }
2689
3266
  async function loadTiers(db, group) {
2690
3267
  const groupId = String(group.id ?? "");
2691
- let rows;
3268
+ let rows2;
2692
3269
  try {
2693
- rows = (await db.query({
3270
+ rows2 = (await db.query({
2694
3271
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
2695
3272
  })).tiers;
2696
3273
  } catch {
2697
- rows = void 0;
3274
+ rows2 = void 0;
2698
3275
  }
2699
- const list = Array.isArray(rows) ? rows.map((row) => rowToTier(row, groupId)) : [];
3276
+ const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
2700
3277
  return resolveTiers(group, list);
2701
3278
  }
2702
3279
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -2707,32 +3284,6 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
2707
3284
  var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
2708
3285
  var SIGNUP_CONTROL_PATH = "/api/signup-control";
2709
3286
 
2710
- // src/signup-control-canonical.ts
2711
- var encoder = new TextEncoder();
2712
- function canonicalValue(value) {
2713
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2714
- return JSON.stringify(value);
2715
- }
2716
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
2717
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
2718
- const record = value;
2719
- const entries = Object.keys(record).sort().flatMap((key) => {
2720
- const item = record[key];
2721
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
2722
- });
2723
- return `{${entries.join(",")}}`;
2724
- }
2725
- function canonicalSignupControl(value) {
2726
- return canonicalValue(value);
2727
- }
2728
- async function signupControlDigest(document) {
2729
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
2730
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2731
- }
2732
- async function createSignupControlEnvelope(document) {
2733
- return { document, digest: await signupControlDigest(document) };
2734
- }
2735
-
2736
3287
  // src/membership-page.ts
2737
3288
  var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
2738
3289
  var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
@@ -2773,10 +3324,10 @@ function boundedString(value, path, errors, max, required = false) {
2773
3324
  errors.push(`${path} must be a string`);
2774
3325
  return void 0;
2775
3326
  }
2776
- const text2 = value.trim();
2777
- if (required && !text2) errors.push(`${path} is required`);
2778
- if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2779
- return text2;
3327
+ const text3 = value.trim();
3328
+ if (required && !text3) errors.push(`${path} is required`);
3329
+ if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
3330
+ return text3;
2780
3331
  }
2781
3332
  function validateMembershipPageDocument(input, options = {}) {
2782
3333
  const errors = [];
@@ -2825,8 +3376,8 @@ function validateMembershipPageDocument(input, options = {}) {
2825
3376
  else {
2826
3377
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
2827
3378
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
2828
- const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2829
- return text2 ? [text2] : [];
3379
+ const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
3380
+ return text3 ? [text3] : [];
2830
3381
  });
2831
3382
  }
2832
3383
  }
@@ -2905,7 +3456,7 @@ function membershipPageDocument(input, options = {}) {
2905
3456
  var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2906
3457
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
2907
3458
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
2908
- var PRICE = /^price_[A-Za-z0-9]{12,}$/;
3459
+ var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
2909
3460
  var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2910
3461
  "schemaVersion",
2911
3462
  "revision",
@@ -2917,14 +3468,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2917
3468
  "application",
2918
3469
  "policy",
2919
3470
  "scheduling",
2920
- "theme"
3471
+ "theme",
3472
+ "giftOffers"
2921
3473
  ]);
2922
3474
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2923
3475
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2924
3476
  function unknownKeys(value, allowed, path, errors) {
2925
3477
  for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2926
3478
  }
2927
- function text(value, path, errors, max = 65536) {
3479
+ function text2(value, path, errors, max = 65536) {
2928
3480
  if (typeof value !== "string" || !value.trim()) {
2929
3481
  errors.push(`${path} is required`);
2930
3482
  return "";
@@ -2947,25 +3499,25 @@ function tiersOf(value, errors) {
2947
3499
  return [];
2948
3500
  }
2949
3501
  unknownKeys(item, TIER_KEYS2, path, errors);
2950
- const id2 = text(item.id, `${path}.id`, errors, 80);
2951
- if (id2 && !TIER.test(id2)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2952
- if (seen.has(id2)) errors.push(`${path}.id duplicates "${id2}"`);
2953
- seen.add(id2);
2954
- const name = text(item.name, `${path}.name`, errors, 160);
3502
+ const id4 = text2(item.id, `${path}.id`, errors, 80);
3503
+ if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3504
+ if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
3505
+ seen.add(id4);
3506
+ const name = text2(item.name, `${path}.name`, errors, 160);
2955
3507
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
2956
3508
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
2957
3509
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
2958
3510
  errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
2959
3511
  }
2960
- if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
3512
+ if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
2961
3513
  errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2962
3514
  }
2963
3515
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
2964
3516
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
2965
3517
  }
2966
- const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
3518
+ const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
2967
3519
  return [{
2968
- id: id2,
3520
+ id: id4,
2969
3521
  name,
2970
3522
  available: item.available === true,
2971
3523
  sortOrder: Number(item.sortOrder) || 0,
@@ -3012,7 +3564,7 @@ function policyOf(value, errors) {
3012
3564
  const result = {};
3013
3565
  for (const key of allowed) {
3014
3566
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
3015
- result[key] = text(input[key], `document.policy.${key}`, errors);
3567
+ result[key] = text2(input[key], `document.policy.${key}`, errors);
3016
3568
  }
3017
3569
  return result;
3018
3570
  }
@@ -3026,12 +3578,18 @@ function themeOf(value, errors) {
3026
3578
  }
3027
3579
  unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
3028
3580
  if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
3581
+ let serializedLength = 0;
3029
3582
  for (const [key, token] of Object.entries(tokens)) {
3030
- if (!/^--[a-z0-9-]{1,100}$/.test(key)) errors.push(`document.theme.tokens.${key} must be a CSS custom-property name`);
3031
- if (typeof token !== "string" && typeof token !== "number" || String(token).length > 1e3) {
3032
- errors.push(`document.theme.tokens.${key} must be a bounded string or number`);
3583
+ if (!/^--ui-[a-z0-9][a-z0-9-]{0,94}$/.test(key)) {
3584
+ errors.push(`document.theme.tokens.${key} must be an allowlisted --ui-* custom property`);
3585
+ }
3586
+ const rendered = String(token).trim();
3587
+ serializedLength += key.length + rendered.length + 4;
3588
+ if (typeof token !== "string" && typeof token !== "number" || typeof token === "number" && !Number.isFinite(token) || !rendered || rendered.length > 1e3 || /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i.test(rendered)) {
3589
+ errors.push(`document.theme.tokens.${key} must be a bounded breakout-safe CSS value`);
3033
3590
  }
3034
3591
  }
3592
+ if (serializedLength > 65536) errors.push("document.theme.tokens exceeds the serialized size bound");
3035
3593
  return { tokens };
3036
3594
  }
3037
3595
  function validateSignupControlDocument(input) {
@@ -3044,9 +3602,9 @@ function validateSignupControlDocument(input) {
3044
3602
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
3045
3603
  const source = recordOf(root.source);
3046
3604
  const target = recordOf(root.target);
3047
- const appId = text(source?.appId, "document.source.appId", errors, 63);
3048
- const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
3049
- const environment = text(target?.environment, "document.target.environment", errors, 63);
3605
+ const appId = text2(source?.appId, "document.source.appId", errors, 63);
3606
+ const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
3607
+ const environment = text2(target?.environment, "document.target.environment", errors, 63);
3050
3608
  if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
3051
3609
  if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
3052
3610
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -3067,6 +3625,8 @@ function validateSignupControlDocument(input) {
3067
3625
  else scheduling = root.scheduling;
3068
3626
  }
3069
3627
  const theme = themeOf(root.theme, errors);
3628
+ const giftOffers = root.giftOffers === void 0 ? void 0 : validateGiftOfferCatalog(root.giftOffers, new Set(tiers2.map((tier) => tier.id)));
3629
+ if (giftOffers && !giftOffers.ok) errors.push(...giftOffers.errors);
3070
3630
  if (errors.length || !page.ok) return { ok: false, errors };
3071
3631
  return { ok: true, value: {
3072
3632
  schemaVersion: 1,
@@ -3079,10 +3639,121 @@ function validateSignupControlDocument(input) {
3079
3639
  ...application ? { application } : {},
3080
3640
  ...policy ? { policy } : {},
3081
3641
  ...scheduling ? { scheduling } : {},
3082
- ...theme ? { theme } : {}
3642
+ ...theme ? { theme } : {},
3643
+ ...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
3083
3644
  } };
3084
3645
  }
3085
3646
 
3647
+ // src/signup-control-theme.ts
3648
+ var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
3649
+ var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
3650
+ var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
3651
+ var DIGEST = /^sha256:[0-9a-f]{64}$/;
3652
+ var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
3653
+ var MAX_TOKENS = 256;
3654
+ var MAX_VALUE_LENGTH = 1e3;
3655
+ var MAX_SERIALIZED_LENGTH = 65536;
3656
+ function validatedTokens(input) {
3657
+ const entries = Object.entries(input ?? {});
3658
+ if (entries.length > MAX_TOKENS) {
3659
+ throw new TypeError(`signup-control theme supports at most ${MAX_TOKENS} tokens`);
3660
+ }
3661
+ const tokens = {};
3662
+ let serializedLength = 0;
3663
+ for (const [name, raw] of entries.sort(([left], [right]) => left.localeCompare(right))) {
3664
+ if (!TOKEN_NAME.test(name)) {
3665
+ throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
3666
+ }
3667
+ if (typeof raw !== "string" && typeof raw !== "number" || typeof raw === "number" && !Number.isFinite(raw)) {
3668
+ throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
3669
+ }
3670
+ const value = String(raw).trim();
3671
+ if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
3672
+ throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
3673
+ }
3674
+ serializedLength += name.length + value.length + 4;
3675
+ if (serializedLength > MAX_SERIALIZED_LENGTH) {
3676
+ throw new TypeError("signup-control theme exceeds the serialized size bound");
3677
+ }
3678
+ tokens[name] = value;
3679
+ }
3680
+ return tokens;
3681
+ }
3682
+ function cssFor(tokens) {
3683
+ const declarations = Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`);
3684
+ return declarations.length ? `:root {
3685
+ ${declarations.join("\n")}
3686
+ }` : "";
3687
+ }
3688
+ function stateFor(revision, options) {
3689
+ if (!revision) {
3690
+ return {
3691
+ css: cssFor(validatedTokens(options.bootstrapTokens)),
3692
+ metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
3693
+ };
3694
+ }
3695
+ if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
3696
+ throw new TypeError("signup-control theme revision metadata is invalid");
3697
+ }
3698
+ return {
3699
+ css: cssFor(validatedTokens(revision.theme?.tokens)),
3700
+ metadata: {
3701
+ source: "active",
3702
+ revision: revision.revision,
3703
+ digest: revision.digest,
3704
+ cacheKey: `signup-control:${revision.digest}`
3705
+ }
3706
+ };
3707
+ }
3708
+ function attributes(metadata) {
3709
+ return [
3710
+ `id="${SIGNUP_CONTROL_THEME_STYLE_ID}"`,
3711
+ `data-signup-control-source="${metadata.source}"`,
3712
+ `data-signup-control-cache-key="${metadata.cacheKey}"`,
3713
+ ...metadata.revision === null ? [] : [`data-signup-control-revision="${metadata.revision}"`],
3714
+ ...metadata.digest === null ? [] : [`data-signup-control-digest="${metadata.digest}"`]
3715
+ ].join(" ");
3716
+ }
3717
+ function renderSignupControlThemeHead(revision, options = {}) {
3718
+ const { css, metadata } = stateFor(revision, options);
3719
+ return { css, metadata, head: `<style ${attributes(metadata)}>${css}</style>` };
3720
+ }
3721
+ function injectSignupControlThemeHead(html, rendered, marker = SIGNUP_CONTROL_THEME_MARKER) {
3722
+ const existing = /<style\b(?=[^>]*\bid\s*=\s*["']odla-signup-control-theme["'])[^>]*>[\s\S]*?<\/style>/gi;
3723
+ const existingBlocks = html.match(existing) ?? [];
3724
+ if (existingBlocks.length > 1) {
3725
+ throw new TypeError("signup-control theme style block must appear at most once");
3726
+ }
3727
+ if (existingBlocks.length === 1) return html.replace(existing, rendered.head);
3728
+ const markerIndex = html.indexOf(marker);
3729
+ if (markerIndex >= 0) {
3730
+ if (html.indexOf(marker, markerIndex + marker.length) >= 0) {
3731
+ throw new TypeError("signup-control theme marker must appear exactly once");
3732
+ }
3733
+ return `${html.slice(0, markerIndex)}${rendered.head}${html.slice(markerIndex + marker.length)}`;
3734
+ }
3735
+ const closingHead = html.search(/<\/head\s*>/i);
3736
+ if (closingHead < 0) throw new TypeError("signup-control theme requires an HTML head");
3737
+ return `${html.slice(0, closingHead)}${rendered.head}${html.slice(closingHead)}`;
3738
+ }
3739
+ function hydrateSignupControlTheme(target, revision, options = {}) {
3740
+ const next = stateFor(revision, options);
3741
+ const currentSource = target.getAttribute("data-signup-control-source");
3742
+ const currentRevision = target.getAttribute("data-signup-control-revision");
3743
+ const currentDigest = target.getAttribute("data-signup-control-digest");
3744
+ if (currentSource === next.metadata.source && currentRevision === (next.metadata.revision === null ? null : String(next.metadata.revision)) && currentDigest === next.metadata.digest) {
3745
+ return false;
3746
+ }
3747
+ target.textContent = next.css;
3748
+ target.setAttribute("data-signup-control-source", next.metadata.source);
3749
+ target.setAttribute("data-signup-control-cache-key", next.metadata.cacheKey);
3750
+ if (next.metadata.revision === null) target.removeAttribute("data-signup-control-revision");
3751
+ else target.setAttribute("data-signup-control-revision", String(next.metadata.revision));
3752
+ if (next.metadata.digest === null) target.removeAttribute("data-signup-control-digest");
3753
+ else target.setAttribute("data-signup-control-digest", next.metadata.digest);
3754
+ return true;
3755
+ }
3756
+
3086
3757
  // src/membership-markdown.ts
3087
3758
  var MAX_BENEFITS2 = 24;
3088
3759
  var MAX_MARKDOWN2 = 65536;
@@ -3110,9 +3781,9 @@ function markdownBenefits(lines, path, errors) {
3110
3781
  errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
3111
3782
  continue;
3112
3783
  }
3113
- const text2 = (match[1] ?? "").trim();
3114
- if (text2.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
3115
- if (text2) benefits.push(text2);
3784
+ const text3 = (match[1] ?? "").trim();
3785
+ if (text3.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
3786
+ if (text3) benefits.push(text3);
3116
3787
  }
3117
3788
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
3118
3789
  return benefits.slice(0, MAX_BENEFITS2);
@@ -3212,7 +3883,7 @@ var exclusions = (chapterId, paidTierIds) => [
3212
3883
  id: chapterId,
3213
3884
  reason: "provider_identity",
3214
3885
  paths: [
3215
- ...paidTierIds.map((id2) => `tiers[${id2}].stripePriceId`),
3886
+ ...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
3216
3887
  "groups.stripePublishableKey",
3217
3888
  "applications.stripeCustomerId",
3218
3889
  "applications.stripeSubscriptionId",
@@ -3254,9 +3925,9 @@ async function exportChapterPromotion(input) {
3254
3925
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
3255
3926
  receiptDigests: [input.digest]
3256
3927
  };
3257
- const artifact = (kind, id2, content) => ({
3928
+ const artifact = (kind, id4, content) => ({
3258
3929
  kind,
3259
- id: id2,
3930
+ id: id4,
3260
3931
  sourceRevision: revision,
3261
3932
  provenance: promotionProvenance,
3262
3933
  content: (0, import_promotion.canonicalPromotionValue)(content)