@odla-ai/chapter 0.35.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,9 +20,13 @@ 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,
@@ -55,6 +59,7 @@ __export(index_exports, {
55
59
  clerkListUsers: () => clerkListUsers,
56
60
  clerkSetRole: () => clerkSetRole,
57
61
  clerkUserRequest: () => clerkUserRequest,
62
+ createAdmissionGrantEnvelope: () => createAdmissionGrantEnvelope,
58
63
  createChapterIntegration: () => createChapterIntegration,
59
64
  createClerkInvitation: () => createClerkInvitation,
60
65
  createClerkUser: () => createClerkUser,
@@ -65,12 +70,15 @@ __export(index_exports, {
65
70
  emailGroupFrom: () => emailGroupFrom,
66
71
  endForSlot: () => endForSlot,
67
72
  exportChapterPromotion: () => exportChapterPromotion,
73
+ findAdmissionGrant: () => findAdmissionGrant,
68
74
  findApplicationRef: () => findApplicationRef,
75
+ findGiftOffer: () => findGiftOffer,
69
76
  findTier: () => findTier,
70
77
  firstPaymentPatch: () => firstPaymentPatch,
71
78
  formatChapterCopy: () => formatChapterCopy,
72
79
  formationFields: () => formationFields,
73
80
  getVaultSecret: () => getVaultSecret,
81
+ giftOfferActive: () => giftOfferActive,
74
82
  hasDisclaimerAck: () => hasDisclaimerAck,
75
83
  hydrateSignupControlTheme: () => hydrateSignupControlTheme,
76
84
  injectSignupControlThemeHead: () => injectSignupControlThemeHead,
@@ -83,8 +91,10 @@ __export(index_exports, {
83
91
  joinConfig: () => joinConfig,
84
92
  joinConfigTiers: () => joinConfigTiers,
85
93
  leaderCrmConfig: () => leaderCrmConfig,
94
+ loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
86
95
  loadOfferableTiers: () => loadOfferableTiers,
87
96
  loadTiers: () => loadTiers,
97
+ materializeAdmissionGrants: () => materializeAdmissionGrants,
88
98
  meetingCreateRow: () => meetingCreateRow,
89
99
  meetingRescheduleUpdate: () => meetingRescheduleUpdate,
90
100
  memberApplication: () => memberApplication,
@@ -92,6 +102,7 @@ __export(index_exports, {
92
102
  membershipPageDocument: () => membershipPageDocument,
93
103
  membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
94
104
  networkSourceTag: () => networkSourceTag,
105
+ normalizeAdmissionEmail: () => normalizeAdmissionEmail,
95
106
  normalizeSharedRecord: () => normalizeSharedRecord,
96
107
  normalizeWebhookEvent: () => normalizeWebhookEvent,
97
108
  offerableTiers: () => offerableTiers,
@@ -100,6 +111,7 @@ __export(index_exports, {
100
111
  planDelivery: () => planDelivery,
101
112
  projectApplicant: () => projectApplicant,
102
113
  projectSharedRecord: () => projectSharedRecord,
114
+ publicGiftOffers: () => publicGiftOffers,
103
115
  reconcileMeetings: () => reconcileMeetings,
104
116
  refundedPatch: () => refundedPatch,
105
117
  render: () => render,
@@ -110,6 +122,7 @@ __export(index_exports, {
110
122
  resolveApplication: () => resolveApplication,
111
123
  resolveAuth: () => resolveAuth,
112
124
  resolveChapterCopy: () => resolveChapterCopy,
125
+ resolveJoinContinuation: () => resolveJoinContinuation,
113
126
  resolveLeaderFormation: () => resolveLeaderFormation,
114
127
  resolvePipeline: () => resolvePipeline,
115
128
  resolveScheduling: () => resolveScheduling,
@@ -132,6 +145,8 @@ __export(index_exports, {
132
145
  tierPayable: () => tierPayable,
133
146
  updateClerkUserMetadata: () => updateClerkUserMetadata,
134
147
  updateClerkUserMetadataByEmail: () => updateClerkUserMetadataByEmail,
148
+ validateAdmissionGrantDocument: () => validateAdmissionGrantDocument,
149
+ validateGiftOfferCatalog: () => validateGiftOfferCatalog,
135
150
  validateMembershipPageDocument: () => validateMembershipPageDocument,
136
151
  validateScheduling: () => validateScheduling,
137
152
  validateSignupControlDocument: () => validateSignupControlDocument,
@@ -157,6 +172,213 @@ async function applicationIdForSubmission(submissionId) {
157
172
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
158
173
  }
159
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
+
160
382
  // src/member.ts
161
383
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
162
384
  var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
@@ -208,9 +430,9 @@ function resolveApplication(a) {
208
430
  validateEmail: a?.validateEmail ?? true
209
431
  };
210
432
  }
211
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
433
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
212
434
  function isValidEmail(value) {
213
- return typeof value === "string" && EMAIL_RE.test(value);
435
+ return typeof value === "string" && EMAIL_RE2.test(value);
214
436
  }
215
437
  function clampArray(value, max) {
216
438
  if (!Array.isArray(value)) return value;
@@ -252,14 +474,23 @@ async function submitApplication(db, chapter, fields, opts) {
252
474
  if (app.requireDisclaimerAck && !acked) {
253
475
  return { ok: false, error: "disclaimerAck is required" };
254
476
  }
255
- const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
256
- 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 };
257
479
  for (const f of [...app.required, ...app.optional]) {
258
480
  if (fieldStates[f]?.visible === false) continue;
259
481
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
260
482
  }
261
483
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
262
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;
263
494
  if (typeof fields.tierId === "string" && fields.tierId) {
264
495
  if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
265
496
  return { ok: false, error: "tierId is not an offered tier" };
@@ -267,11 +498,20 @@ async function submitApplication(db, chapter, fields, opts) {
267
498
  row.tierId = fields.tierId;
268
499
  }
269
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
+ }
270
510
  const { duplicate } = await db.transact(
271
- [{ t: "update", ns: "applications", id: id2, attrs: row }],
511
+ ops,
272
512
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
273
513
  );
274
- 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 };
275
515
  }
276
516
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
277
517
  return {
@@ -291,93 +531,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
291
531
  };
292
532
  }
293
533
 
294
- // src/schema.ts
295
- function attr(type, flags = {}) {
296
- return {
297
- type,
298
- unique: flags.unique ?? false,
299
- indexed: flags.indexed ?? false,
300
- optional: flags.optional ?? false
301
- };
302
- }
303
- var id = () => attr("string", { unique: true, indexed: true });
304
- var admins = {
305
- attrs: {
306
- id: id(),
307
- email: attr("string", { unique: true, indexed: true }),
308
- name: attr("string", { optional: true }),
309
- note: attr("string", { optional: true })
310
- }
311
- };
312
- var superAdmins = {
313
- attrs: {
314
- id: id(),
315
- email: attr("string", { unique: true, indexed: true }),
316
- note: attr("string", { optional: true }),
317
- createdAt: attr("number", { indexed: true })
318
- }
319
- };
320
- var networkNotes = {
321
- attrs: {
322
- id: id(),
323
- recordKey: attr("string", { indexed: true }),
324
- targetId: attr("string", { indexed: true }),
325
- recordType: attr("string", { indexed: true }),
326
- recordId: attr("string", { indexed: true }),
327
- body: attr("string"),
328
- authorId: attr("string", { indexed: true }),
329
- authorEmail: attr("string", { optional: true }),
330
- createdAt: attr("number", { indexed: true })
331
- }
332
- };
333
- var signupControls = {
334
- attrs: {
335
- id: id(),
336
- sourceId: attr("string", { indexed: true }),
337
- targetId: attr("string", { indexed: true }),
338
- environment: attr("string", { indexed: true }),
339
- revision: attr("number", { indexed: true }),
340
- digest: attr("string", { indexed: true }),
341
- document: attr("json"),
342
- active: attr("boolean", { indexed: true }),
343
- receivedAt: attr("number", { indexed: true })
344
- }
345
- };
346
- var applications = {
347
- attrs: {
348
- id: id(),
349
- firstName: attr("string"),
350
- lastName: attr("string"),
351
- email: attr("string", { indexed: true }),
352
- referral: attr("string"),
353
- referralName: attr("string", { optional: true }),
354
- whoYouAre: attr("string"),
355
- focus: attr("json"),
356
- linkedin: attr("string", { optional: true }),
357
- message: attr("string"),
358
- status: attr("string", { indexed: true }),
359
- createdAt: attr("number", { indexed: true }),
360
- meetingAt: attr("number", { indexed: true, optional: true }),
361
- meetingLink: attr("string", { optional: true }),
362
- clerkUserId: attr("string", { indexed: true, optional: true }),
363
- // One-time/effect-repair marker. Absent means /api/me should copy the
364
- // allowlisted application snapshot into Clerk private_metadata and remove
365
- // the former public_metadata copy.
366
- clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
367
- phone: attr("string", { optional: true }),
368
- state: attr("string", { optional: true }),
369
- groupId: attr("string", { indexed: true, optional: true }),
370
- // The tier this application joined on. Absent on rows that predate tiers.
371
- tierId: attr("string", { indexed: true, optional: true }),
372
- stripeCustomerId: attr("string", { indexed: true, optional: true }),
373
- stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
374
- renewalAt: attr("number", { optional: true }),
375
- disclaimerAckAt: attr("number", { optional: true }),
376
- refundPolicyAckAt: attr("number", { optional: true }),
377
- prepEmailSentAt: attr("number", { optional: true }),
378
- canceled: attr("boolean", { optional: true })
379
- }
380
- };
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
+ });
381
541
  var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
382
542
  "firstName",
383
543
  "lastName",
@@ -391,25 +551,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
391
551
  "phone",
392
552
  "state"
393
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
+ };
394
594
  function applicationsFor(application) {
395
595
  const attrs = {};
396
- for (const [name, spec] of Object.entries(applications.attrs)) {
397
- attrs[name] = {
398
- ...spec,
399
- optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
400
- };
596
+ for (const [name, spec] of Object.entries(applicationAttrs)) {
597
+ attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
401
598
  }
402
599
  const apply = (field, optional) => {
403
600
  const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
404
- if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
405
- throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
406
- }
407
- if (existing && existing.type !== "string") {
408
- throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
409
- }
410
- const spec = existing ? { ...existing, optional } : attr("string", { optional });
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`);
411
603
  Object.defineProperty(attrs, field, {
412
- value: spec,
604
+ value: existing ? { ...existing, optional } : attr("string", { optional }),
413
605
  enumerable: true,
414
606
  configurable: true,
415
607
  writable: true
@@ -419,75 +611,270 @@ function applicationsFor(application) {
419
611
  for (const field of application.optional) apply(field, true);
420
612
  return { attrs };
421
613
  }
422
- 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 = {
423
624
  attrs: {
424
625
  id: id(),
425
- name: attr("string"),
426
- standardPriceCents: attr("number"),
427
- foundingDiscountCents: attr("number"),
428
- stripePriceId: attr("string", { optional: true }),
429
- stripePublishableKey: attr("string", { optional: true }),
430
- notificationEmail: attr("string"),
431
- replyTo: attr("string"),
432
- debugEmail: attr("string", { optional: true }),
433
- calendarLink: attr("string", { optional: true }),
434
- disclaimerText: attr("string"),
435
- refundPolicyText: attr("string"),
436
- trustCopy: attr("string"),
437
- commitmentText: attr("string", { optional: true }),
438
- normsText: attr("string", { optional: true }),
439
- emailTemplates: attr("json"),
440
- schedulingJson: attr("json", { optional: true }),
441
- createdAt: attr("number", { indexed: true })
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 })
442
645
  }
443
646
  };
444
- var tiers = {
647
+ var admissionGrantRevisionsEntity = {
445
648
  attrs: {
446
649
  id: id(),
447
- groupId: attr("string", { indexed: true }),
448
- name: attr("string"),
449
- priceCents: attr("number"),
450
- stripePriceId: attr("string", { optional: true }),
451
- blurb: attr("string", { optional: true }),
452
- sortOrder: attr("number", { indexed: true }),
453
- active: attr("boolean", { indexed: true }),
454
- createdAt: attr("number", { indexed: true })
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 })
455
842
  }
456
843
  };
457
844
  var meetings = {
458
845
  attrs: {
459
- id: id(),
460
- applicationId: attr("string", { indexed: true }),
461
- groupId: attr("string", { indexed: true }),
462
- startAt: attr("number", { indexed: true }),
463
- endAt: attr("number"),
464
- timezone: attr("string"),
465
- status: attr("string", { indexed: true }),
466
- googleEventId: attr("string", { indexed: true, optional: true }),
467
- meetUrl: attr("string", { optional: true }),
468
- htmlLink: attr("string", { optional: true }),
469
- drift: attr("string", { indexed: true, optional: true }),
470
- driftGoogleStartAt: attr("number", { optional: true }),
471
- driftDetectedAt: attr("number", { optional: true }),
472
- adoptedFromGoogleAt: attr("number", { optional: true }),
473
- createdAt: attr("number", { indexed: true })
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 })
474
861
  }
475
862
  };
476
863
  var emailLog = {
477
864
  attrs: {
478
- id: id(),
479
- groupId: attr("string", { indexed: true }),
480
- applicationId: attr("string", { indexed: true, optional: true }),
481
- to: attr("string", { indexed: true }),
482
- template: attr("string", { indexed: true }),
483
- subject: attr("string"),
484
- body: attr("string", { optional: true }),
485
- transport: attr("string"),
486
- messageId: attr("string", { optional: true }),
487
- redirected: attr("boolean", { optional: true }),
488
- dedupeKey: attr("string", { indexed: true, optional: true }),
489
- error: attr("string", { optional: true }),
490
- sentAt: attr("number", { indexed: true })
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 })
491
878
  }
492
879
  };
493
880
  function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
@@ -498,6 +885,13 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
498
885
  entities.tiers = tiers;
499
886
  entities.meetings = meetings;
500
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;
501
895
  if (includeSignupControl) entities.signupControls = signupControls;
502
896
  }
503
897
  if (auth.source === "table") entities.admins = admins;
@@ -782,6 +1176,7 @@ var DEFAULT_ADMIN_COPY = {
782
1176
  overview: "Overview",
783
1177
  billing: "Billing",
784
1178
  people: "People",
1179
+ admission: "Admission",
785
1180
  network: "Network",
786
1181
  portfolio: "Portfolio",
787
1182
  settings: "Settings",
@@ -892,6 +1287,26 @@ var DEFAULT_ADMIN_COPY = {
892
1287
  reschedule: "Reschedule",
893
1288
  cancel: "Cancel"
894
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
+ },
895
1310
  network: {
896
1311
  title: "Share to a follower",
897
1312
  allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
@@ -990,6 +1405,19 @@ var DEFAULT_JOIN_COPY = {
990
1405
  label: "You're booked",
991
1406
  calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
992
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."
993
1421
  }
994
1422
  };
995
1423
  var DEFAULT_MEMBERS_COPY = {
@@ -1017,6 +1445,26 @@ var DEFAULT_MEMBERS_COPY = {
1017
1445
  full: {
1018
1446
  welcome: "Welcome back."
1019
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
+ },
1020
1468
  reschedule: {
1021
1469
  changeTime: "Change your time",
1022
1470
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
@@ -1132,13 +1580,13 @@ function resolveLeaderFormation(input, crm) {
1132
1580
  }
1133
1581
  function formationFields(crm, formation) {
1134
1582
  const def = crm.type(formation.type);
1135
- return [...formation.required, ...formation.optional].map((id2) => {
1136
- const field = def.fields[id2];
1583
+ return [...formation.required, ...formation.optional].map((id4) => {
1584
+ const field = def.fields[id4];
1137
1585
  return {
1138
- id: id2,
1139
- label: field.label ?? id2,
1586
+ id: id4,
1587
+ label: field.label ?? id4,
1140
1588
  type: field.type,
1141
- required: formation.required.includes(id2),
1589
+ required: formation.required.includes(id4),
1142
1590
  ...field.options ? { options: field.options } : {}
1143
1591
  };
1144
1592
  });
@@ -1297,9 +1745,9 @@ function isResolvedCrm(x) {
1297
1745
  }
1298
1746
  function defineChapter(config) {
1299
1747
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
1300
- const { id: id2, name } = config;
1301
- if (typeof id2 !== "string" || !SLUG3.test(id2)) {
1302
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
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)}`);
1303
1751
  }
1304
1752
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
1305
1753
  const mode = config.mode ?? "chapter";
@@ -1382,7 +1830,7 @@ function defineChapter(config) {
1382
1830
  };
1383
1831
  const chapter = {
1384
1832
  config,
1385
- id: id2,
1833
+ id: id4,
1386
1834
  name,
1387
1835
  mode,
1388
1836
  brand,
@@ -1596,8 +2044,8 @@ function tierPayable(tier, group, hasSecretKey) {
1596
2044
  function byOrder(a, b) {
1597
2045
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1598
2046
  }
1599
- function resolveTiers(group, rows = []) {
1600
- const stored = rows.filter((tier) => tier.active);
2047
+ function resolveTiers(group, rows2 = []) {
2048
+ const stored = rows2.filter((tier) => tier.active);
1601
2049
  if (stored.length) return [...stored].sort(byOrder);
1602
2050
  return [{
1603
2051
  id: "standard",
@@ -1719,10 +2167,10 @@ function planDelivery(input) {
1719
2167
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1720
2168
  const to = redirect ? input.group.debugEmail : input.to;
1721
2169
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1722
- const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
2170
+ const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
1723
2171
 
1724
2172
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1725
- return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
2173
+ return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
1726
2174
  }
1727
2175
 
1728
2176
  // src/notify.ts
@@ -1757,9 +2205,9 @@ async function sendTemplated(deps, input) {
1757
2205
  error = e instanceof Error ? e.message : String(e);
1758
2206
  }
1759
2207
  }
1760
- const id2 = deps.newId();
2208
+ const id4 = deps.newId();
1761
2209
  const row = {
1762
- id: id2,
2210
+ id: id4,
1763
2211
  groupId: input.group.id,
1764
2212
  to: decision.to,
1765
2213
  template: input.template,
@@ -1773,7 +2221,7 @@ async function sendTemplated(deps, input) {
1773
2221
  ...messageId ? { messageId } : {},
1774
2222
  ...error ? { error } : {}
1775
2223
  };
1776
- 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}` });
1777
2225
  return error ? { sent: false, reason: error } : { sent: true };
1778
2226
  }
1779
2227
  function emailGroupFrom(row) {
@@ -1809,8 +2257,26 @@ function findApplicationRef(obj) {
1809
2257
  const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
1810
2258
  return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
1811
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
+ }
1812
2269
  function normalizeWebhookEvent(event) {
1813
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
+ }
1814
2280
  const ref = findApplicationRef(obj);
1815
2281
  switch (event.type) {
1816
2282
  case "invoice.paid": {
@@ -1844,6 +2310,112 @@ function canceledPatch() {
1844
2310
  return { canceled: true };
1845
2311
  }
1846
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
+
1847
2419
  // src/network.ts
1848
2420
  var import_crm5 = require("@odla-ai/crm");
1849
2421
 
@@ -1883,34 +2455,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
1883
2455
  if (readable && raw.length <= 64) return raw;
1884
2456
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
1885
2457
  }
1886
- function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1887
- if ("input" in record && record.version === 2) {
2458
+ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
2459
+ if ("input" in record2 && record2.version === 2) {
1888
2460
  return {
1889
2461
  version: 2,
1890
- sourceId: record.source.siteId,
1891
- sourceRecordId: record.source.recordId,
1892
- type: record.type,
1893
- input: record.input
2462
+ sourceId: record2.source.siteId,
2463
+ sourceRecordId: record2.source.recordId,
2464
+ type: record2.type,
2465
+ input: record2.input
1894
2466
  };
1895
2467
  }
1896
- if ("input" in record) {
2468
+ if ("input" in record2) {
1897
2469
  return {
1898
2470
  version: 1,
1899
2471
  sourceId: fallbackSourceId,
1900
- sourceRecordId: record.hubRecordId,
1901
- type: record.type,
1902
- input: record.input
2472
+ sourceRecordId: record2.hubRecordId,
2473
+ type: record2.type,
2474
+ input: record2.input
1903
2475
  };
1904
2476
  }
1905
- if ("type" in record && record.type === "company") {
1906
- const input = { name: record.name };
2477
+ if ("type" in record2 && record2.type === "company") {
2478
+ const input = { name: record2.name };
1907
2479
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
1908
- if (record[key]) input[key] = record[key];
2480
+ if (record2[key]) input[key] = record2[key];
1909
2481
  }
1910
2482
  return {
1911
2483
  version: 1,
1912
2484
  sourceId: fallbackSourceId,
1913
- sourceRecordId: record.hubRecordId,
2485
+ sourceRecordId: record2.hubRecordId,
1914
2486
  type: "company",
1915
2487
  input
1916
2488
  };
@@ -1918,28 +2490,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1918
2490
  return {
1919
2491
  version: 1,
1920
2492
  sourceId: fallbackSourceId,
1921
- sourceRecordId: record.hubRecordId,
2493
+ sourceRecordId: record2.hubRecordId,
1922
2494
  type: "person",
1923
- input: sharedPersonInput(record)
2495
+ input: sharedPersonInput(record2)
1924
2496
  };
1925
2497
  }
1926
- function sharedRecordFromCrm(crm, record, target, sourceId) {
1927
- if (target.fields && !target.fields[record.type]) {
1928
- throw new Error(`${target.name} does not accept "${record.type}" records`);
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`);
1929
2501
  }
1930
- const def = crm.type(record.type);
1931
- 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];
1932
2504
  if (!fields) {
1933
- 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`);
1934
2506
  }
1935
2507
  const nameField = def.nameField ?? "name";
1936
2508
  const input = {};
1937
2509
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
1938
- const value = record.fields?.[field];
2510
+ const value = record2.fields?.[field];
1939
2511
  if (value !== void 0) input[field] = value;
1940
2512
  }
1941
- if (input[nameField] === void 0) input[nameField] = record.name;
1942
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
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 };
1943
2515
  }
1944
2516
  async function upsertPerson(deps, opts) {
1945
2517
  const email = opts.email.toLowerCase();
@@ -1953,11 +2525,11 @@ async function upsertPerson(deps, opts) {
1953
2525
  const created = await (0, import_crm5.createRecord)(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
1954
2526
  return { recordId: created.id };
1955
2527
  }
1956
- async function findSharedRecord(deps, record, tag) {
2528
+ async function findSharedRecord(deps, record2, tag) {
1957
2529
  const structured = await (0, import_crm5.getRecordByOrigin)(
1958
2530
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
1959
- record.sourceId,
1960
- record.sourceRecordId
2531
+ record2.sourceId,
2532
+ record2.sourceRecordId
1961
2533
  );
1962
2534
  if (structured) return structured;
1963
2535
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -1966,53 +2538,53 @@ async function findSharedRecord(deps, record, tag) {
1966
2538
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1967
2539
  if (found.crm_record?.[0]) return found.crm_record[0];
1968
2540
  }
1969
- const def = deps.crm.type(record.type);
2541
+ const def = deps.crm.type(record2.type);
1970
2542
  const emailField = def.emailField;
1971
- if (emailField && typeof record.input[emailField] === "string") {
1972
- const primaryEmail = record.input[emailField].toLowerCase();
2543
+ if (emailField && typeof record2.input[emailField] === "string") {
2544
+ const primaryEmail = record2.input[emailField].toLowerCase();
1973
2545
  const found = await deps.db.query({
1974
- crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
2546
+ crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
1975
2547
  });
1976
2548
  if (found.crm_record?.[0]) return found.crm_record[0];
1977
2549
  }
1978
- const domain = record.input.domain;
2550
+ const domain = record2.input.domain;
1979
2551
  const domainSlot = def.fields.domain?.slot;
1980
2552
  if (typeof domain === "string" && domainSlot) {
1981
2553
  const found = await deps.db.query({
1982
- crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
2554
+ crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
1983
2555
  });
1984
2556
  if (found.crm_record?.[0]) return found.crm_record[0];
1985
2557
  }
1986
2558
  const nameField = def.nameField ?? "name";
1987
- const name = record.input[nameField];
1988
- 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()) {
1989
2561
  const found = await deps.db.query({
1990
- crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
2562
+ crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
1991
2563
  });
1992
2564
  if (found.crm_record?.[0]) return found.crm_record[0];
1993
2565
  }
1994
2566
  return void 0;
1995
2567
  }
1996
2568
  async function projectSharedRecord(deps, shared, options = {}) {
1997
- const record = normalizeSharedRecord(shared, options.sourceId);
1998
- 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()) {
1999
2571
  throw new Error("type, source site id, and source record id must be non-empty");
2000
2572
  }
2001
- if (options.sourceId && options.sourceId !== record.sourceId) {
2573
+ if (options.sourceId && options.sourceId !== record2.sourceId) {
2002
2574
  throw new Error("signed sender does not match payload source");
2003
2575
  }
2004
- 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);
2005
2577
  const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
2006
- const existing = await findSharedRecord(deps, record, tag);
2578
+ const existing = await findSharedRecord(deps, record2, tag);
2007
2579
  let recordId;
2008
2580
  if (existing && typeof existing.id === "string") {
2009
- 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 });
2010
2582
  recordId = existing.id;
2011
2583
  } else {
2012
- recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
2584
+ recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
2013
2585
  await (0, import_crm5.createRecord)({ ...crmDeps, newId: () => recordId }, {
2014
- type: record.type,
2015
- input: record.input,
2586
+ type: record2.type,
2587
+ input: record2.input,
2016
2588
  mutationId: `share-create:${tag}`
2017
2589
  });
2018
2590
  }
@@ -2022,10 +2594,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
2022
2594
  );
2023
2595
  await (0, import_crm5.upsertRecordOrigin)(crmDeps, {
2024
2596
  recordId,
2025
- sourceId: record.sourceId,
2026
- sourceRecordId: record.sourceRecordId,
2597
+ sourceId: record2.sourceId,
2598
+ sourceRecordId: record2.sourceRecordId,
2027
2599
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
2028
- payloadVersion: record.version
2600
+ payloadVersion: record2.version
2029
2601
  });
2030
2602
  return { recordId };
2031
2603
  }
@@ -2241,9 +2813,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
2241
2813
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
2242
2814
  if (!found.ok) return false;
2243
2815
  const users = await found.json().catch(() => null);
2244
- const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
2245
- if (!id2) return false;
2246
- return updateClerkUserMetadata(secretKey, id2, input, fetchImpl);
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);
2247
2819
  }
2248
2820
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
2249
2821
  const { path, body } = clerkUserRequest(input);
@@ -2299,8 +2871,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
2299
2871
  const user = Array.isArray(data) ? data[0] : void 0;
2300
2872
  return user ? toRecord(user) : null;
2301
2873
  }
2302
- async function clerkGetUser(secretKey, id2, fetchImpl = fetch) {
2303
- 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);
2304
2876
  return data ? toRecord(data) : null;
2305
2877
  }
2306
2878
  async function clerkListUsers(secretKey, fetchImpl = fetch) {
@@ -2309,15 +2881,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
2309
2881
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
2310
2882
  const page = Array.isArray(data) ? data : [];
2311
2883
  for (const u of page) {
2312
- const record = toRecord(u);
2313
- if (record) out.push(record);
2884
+ const record2 = toRecord(u);
2885
+ if (record2) out.push(record2);
2314
2886
  }
2315
2887
  if (page.length < PAGE) break;
2316
2888
  }
2317
2889
  return out;
2318
2890
  }
2319
- async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
2320
- 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`, {
2321
2893
  method: "PATCH",
2322
2894
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
2323
2895
  body: JSON.stringify({ public_metadata: { role } })
@@ -2693,15 +3265,15 @@ function rowToTier(row, groupId) {
2693
3265
  }
2694
3266
  async function loadTiers(db, group) {
2695
3267
  const groupId = String(group.id ?? "");
2696
- let rows;
3268
+ let rows2;
2697
3269
  try {
2698
- rows = (await db.query({
3270
+ rows2 = (await db.query({
2699
3271
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
2700
3272
  })).tiers;
2701
3273
  } catch {
2702
- rows = void 0;
3274
+ rows2 = void 0;
2703
3275
  }
2704
- const list = Array.isArray(rows) ? rows.map((row) => rowToTier(row, groupId)) : [];
3276
+ const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
2705
3277
  return resolveTiers(group, list);
2706
3278
  }
2707
3279
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -2712,32 +3284,6 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
2712
3284
  var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
2713
3285
  var SIGNUP_CONTROL_PATH = "/api/signup-control";
2714
3286
 
2715
- // src/signup-control-canonical.ts
2716
- var encoder = new TextEncoder();
2717
- function canonicalValue(value) {
2718
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2719
- return JSON.stringify(value);
2720
- }
2721
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
2722
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
2723
- const record = value;
2724
- const entries = Object.keys(record).sort().flatMap((key) => {
2725
- const item = record[key];
2726
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
2727
- });
2728
- return `{${entries.join(",")}}`;
2729
- }
2730
- function canonicalSignupControl(value) {
2731
- return canonicalValue(value);
2732
- }
2733
- async function signupControlDigest(document) {
2734
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
2735
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2736
- }
2737
- async function createSignupControlEnvelope(document) {
2738
- return { document, digest: await signupControlDigest(document) };
2739
- }
2740
-
2741
3287
  // src/membership-page.ts
2742
3288
  var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
2743
3289
  var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
@@ -2778,10 +3324,10 @@ function boundedString(value, path, errors, max, required = false) {
2778
3324
  errors.push(`${path} must be a string`);
2779
3325
  return void 0;
2780
3326
  }
2781
- const text2 = value.trim();
2782
- if (required && !text2) errors.push(`${path} is required`);
2783
- if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2784
- return text2;
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;
2785
3331
  }
2786
3332
  function validateMembershipPageDocument(input, options = {}) {
2787
3333
  const errors = [];
@@ -2830,8 +3376,8 @@ function validateMembershipPageDocument(input, options = {}) {
2830
3376
  else {
2831
3377
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
2832
3378
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
2833
- const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2834
- return text2 ? [text2] : [];
3379
+ const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
3380
+ return text3 ? [text3] : [];
2835
3381
  });
2836
3382
  }
2837
3383
  }
@@ -2910,7 +3456,7 @@ function membershipPageDocument(input, options = {}) {
2910
3456
  var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2911
3457
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
2912
3458
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
2913
- var PRICE = /^price_[A-Za-z0-9]{12,}$/;
3459
+ var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
2914
3460
  var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2915
3461
  "schemaVersion",
2916
3462
  "revision",
@@ -2922,14 +3468,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2922
3468
  "application",
2923
3469
  "policy",
2924
3470
  "scheduling",
2925
- "theme"
3471
+ "theme",
3472
+ "giftOffers"
2926
3473
  ]);
2927
3474
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2928
3475
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2929
3476
  function unknownKeys(value, allowed, path, errors) {
2930
3477
  for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2931
3478
  }
2932
- function text(value, path, errors, max = 65536) {
3479
+ function text2(value, path, errors, max = 65536) {
2933
3480
  if (typeof value !== "string" || !value.trim()) {
2934
3481
  errors.push(`${path} is required`);
2935
3482
  return "";
@@ -2952,25 +3499,25 @@ function tiersOf(value, errors) {
2952
3499
  return [];
2953
3500
  }
2954
3501
  unknownKeys(item, TIER_KEYS2, path, errors);
2955
- const id2 = text(item.id, `${path}.id`, errors, 80);
2956
- if (id2 && !TIER.test(id2)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2957
- if (seen.has(id2)) errors.push(`${path}.id duplicates "${id2}"`);
2958
- seen.add(id2);
2959
- const name = text(item.name, `${path}.name`, errors, 160);
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);
2960
3507
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
2961
3508
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
2962
3509
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
2963
3510
  errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
2964
3511
  }
2965
- 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))) {
2966
3513
  errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2967
3514
  }
2968
3515
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
2969
3516
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
2970
3517
  }
2971
- 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);
2972
3519
  return [{
2973
- id: id2,
3520
+ id: id4,
2974
3521
  name,
2975
3522
  available: item.available === true,
2976
3523
  sortOrder: Number(item.sortOrder) || 0,
@@ -3017,7 +3564,7 @@ function policyOf(value, errors) {
3017
3564
  const result = {};
3018
3565
  for (const key of allowed) {
3019
3566
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
3020
- result[key] = text(input[key], `document.policy.${key}`, errors);
3567
+ result[key] = text2(input[key], `document.policy.${key}`, errors);
3021
3568
  }
3022
3569
  return result;
3023
3570
  }
@@ -3055,9 +3602,9 @@ function validateSignupControlDocument(input) {
3055
3602
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
3056
3603
  const source = recordOf(root.source);
3057
3604
  const target = recordOf(root.target);
3058
- const appId = text(source?.appId, "document.source.appId", errors, 63);
3059
- const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
3060
- const environment = text(target?.environment, "document.target.environment", errors, 63);
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);
3061
3608
  if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
3062
3609
  if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
3063
3610
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -3078,6 +3625,8 @@ function validateSignupControlDocument(input) {
3078
3625
  else scheduling = root.scheduling;
3079
3626
  }
3080
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);
3081
3630
  if (errors.length || !page.ok) return { ok: false, errors };
3082
3631
  return { ok: true, value: {
3083
3632
  schemaVersion: 1,
@@ -3090,7 +3639,8 @@ function validateSignupControlDocument(input) {
3090
3639
  ...application ? { application } : {},
3091
3640
  ...policy ? { policy } : {},
3092
3641
  ...scheduling ? { scheduling } : {},
3093
- ...theme ? { theme } : {}
3642
+ ...theme ? { theme } : {},
3643
+ ...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
3094
3644
  } };
3095
3645
  }
3096
3646
 
@@ -3231,9 +3781,9 @@ function markdownBenefits(lines, path, errors) {
3231
3781
  errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
3232
3782
  continue;
3233
3783
  }
3234
- const text2 = (match[1] ?? "").trim();
3235
- if (text2.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
3236
- if (text2) benefits.push(text2);
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);
3237
3787
  }
3238
3788
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
3239
3789
  return benefits.slice(0, MAX_BENEFITS2);
@@ -3333,7 +3883,7 @@ var exclusions = (chapterId, paidTierIds) => [
3333
3883
  id: chapterId,
3334
3884
  reason: "provider_identity",
3335
3885
  paths: [
3336
- ...paidTierIds.map((id2) => `tiers[${id2}].stripePriceId`),
3886
+ ...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
3337
3887
  "groups.stripePublishableKey",
3338
3888
  "applications.stripeCustomerId",
3339
3889
  "applications.stripeSubscriptionId",
@@ -3375,9 +3925,9 @@ async function exportChapterPromotion(input) {
3375
3925
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
3376
3926
  receiptDigests: [input.digest]
3377
3927
  };
3378
- const artifact = (kind, id2, content) => ({
3928
+ const artifact = (kind, id4, content) => ({
3379
3929
  kind,
3380
- id: id2,
3930
+ id: id4,
3381
3931
  sourceRevision: revision,
3382
3932
  provenance: promotionProvenance,
3383
3933
  content: (0, import_promotion.canonicalPromotionValue)(content)