@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.js CHANGED
@@ -15,6 +15,213 @@ async function applicationIdForSubmission(submissionId) {
15
15
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
16
16
  }
17
17
 
18
+ // src/admission-grants-contract.ts
19
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
20
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
21
+
22
+ // src/signup-control-canonical.ts
23
+ var encoder = new TextEncoder();
24
+ function canonicalValue(value) {
25
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
26
+ return JSON.stringify(value);
27
+ }
28
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
29
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
30
+ const record2 = value;
31
+ const entries = Object.keys(record2).sort().flatMap((key) => {
32
+ const item = record2[key];
33
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
34
+ });
35
+ return `{${entries.join(",")}}`;
36
+ }
37
+ function canonicalSignupControl(value) {
38
+ return canonicalValue(value);
39
+ }
40
+ async function signupControlDigest(document) {
41
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
42
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
43
+ }
44
+ async function createSignupControlEnvelope(document) {
45
+ return { document, digest: await signupControlDigest(document) };
46
+ }
47
+
48
+ // src/admission-grants.ts
49
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
50
+ var MAX_GRANTS = 2e3;
51
+ var MAX_NOTE = 2e3;
52
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
53
+ var isAdmissionEmail = (value) => EMAIL_RE.test(value);
54
+ function normalizeAdmissionEmail(email) {
55
+ return email.trim().toLowerCase();
56
+ }
57
+ function validateAdmissionGrantDocument(input) {
58
+ const errors = [];
59
+ const root = input && typeof input === "object" ? input : {};
60
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
61
+ const revision = Number(root.revision);
62
+ const generatedAt = Number(root.generatedAt);
63
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
64
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
65
+ const source = root.source && typeof root.source === "object" ? root.source : {};
66
+ const target = root.target && typeof root.target === "object" ? root.target : {};
67
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
68
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
69
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
70
+ const environment = typeof target.environment === "string" ? target.environment : "";
71
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
72
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
73
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
74
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
75
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
76
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
77
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
78
+ const grants = [];
79
+ const seenIds = /* @__PURE__ */ new Set();
80
+ const seenEmails = /* @__PURE__ */ new Set();
81
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
82
+ const grant = raw && typeof raw === "object" ? raw : {};
83
+ const id4 = typeof grant.id === "string" ? grant.id : "";
84
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
85
+ const normalized = normalizeAdmissionEmail(email);
86
+ const decision = grant.decision;
87
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
88
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
89
+ if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
90
+ if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
91
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
92
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
93
+ if (decision !== "waive_interview" && decision !== "require_interview") {
94
+ errors.push(`document.grants[${index}].decision is invalid`);
95
+ }
96
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
97
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
98
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
99
+ }
100
+ seenIds.add(id4);
101
+ seenEmails.add(normalized);
102
+ grants.push({
103
+ id: id4,
104
+ email,
105
+ decision,
106
+ ...note ? { note } : {},
107
+ ...expiresAt !== void 0 ? { expiresAt } : {}
108
+ });
109
+ }
110
+ if (errors.length) return { ok: false, errors };
111
+ return {
112
+ ok: true,
113
+ value: {
114
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
115
+ revision,
116
+ generatedAt,
117
+ source: { appId: sourceId, actor: sourceActor },
118
+ target: { chapterId, environment },
119
+ grants
120
+ }
121
+ };
122
+ }
123
+ async function createAdmissionGrantEnvelope(document) {
124
+ const checked = validateAdmissionGrantDocument(document);
125
+ if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
126
+ return { document: checked.value, digest: await signupControlDigest(checked.value) };
127
+ }
128
+ var rows = (value) => Array.isArray(value) ? value : [];
129
+ function asGrant(row) {
130
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
131
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
132
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
133
+ return row;
134
+ }
135
+ async function findAdmissionGrant(db, targetId, email, now) {
136
+ const emailNormalized = normalizeAdmissionEmail(email);
137
+ if (!isAdmissionEmail(emailNormalized)) return null;
138
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
139
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
140
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
141
+ )).sort((a, b) => {
142
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
143
+ return scope || b.createdAt - a.createdAt;
144
+ });
145
+ return active[0] ?? null;
146
+ }
147
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
148
+ const where = { sourceId, targetId, environment };
149
+ if (db.aggregate) {
150
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
151
+ }
152
+ const found = await db.query({ admissionGrantRevisions: {} });
153
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
154
+ }
155
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
156
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
157
+ const result = await db.query({ admissionGrantRevisions: { $: {
158
+ where: { sourceId, targetId, environment },
159
+ order: { revision: "desc" },
160
+ limit: 1
161
+ } } });
162
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
163
+ return row ? row : null;
164
+ }
165
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
166
+ const { document, digest } = envelope;
167
+ const current = rows((await db.query({ admissionGrants: { $: {
168
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
169
+ limit: MAX_GRANTS
170
+ } } })).admissionGrants);
171
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
172
+ const ops = [];
173
+ for (const row of current) {
174
+ const id4 = String(row.id ?? "");
175
+ if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
176
+ active: false,
177
+ revokedAt: receivedAt,
178
+ revokedBy: document.source.actor,
179
+ updatedAt: receivedAt,
180
+ updatedBy: document.source.actor
181
+ } });
182
+ }
183
+ for (const grant of document.grants) {
184
+ const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
185
+ const existing = current.find((row) => row.id === id4);
186
+ ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
187
+ id: id4,
188
+ sourceGrantId: grant.id,
189
+ sourceScope: "parent",
190
+ sourceId: document.source.appId,
191
+ targetId: document.target.chapterId,
192
+ email: grant.email,
193
+ emailNormalized: normalizeAdmissionEmail(grant.email),
194
+ decision: grant.decision,
195
+ ...grant.note ? { note: grant.note } : {},
196
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
197
+ active: true,
198
+ sourceRevision: document.revision,
199
+ createdAt: Number(existing?.createdAt ?? receivedAt),
200
+ createdBy: String(existing?.createdBy ?? document.source.actor),
201
+ updatedAt: receivedAt,
202
+ updatedBy: document.source.actor
203
+ } });
204
+ const retract = [
205
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
206
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
207
+ ];
208
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
209
+ }
210
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
211
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
212
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
213
+ id: revisionId,
214
+ sourceId: document.source.appId,
215
+ targetId: document.target.chapterId,
216
+ environment: document.target.environment,
217
+ revision: document.revision,
218
+ digest,
219
+ active: true,
220
+ receivedAt
221
+ } });
222
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
223
+ }
224
+
18
225
  // src/member.ts
19
226
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
20
227
  var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
@@ -66,9 +273,9 @@ function resolveApplication(a) {
66
273
  validateEmail: a?.validateEmail ?? true
67
274
  };
68
275
  }
69
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
276
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
70
277
  function isValidEmail(value) {
71
- return typeof value === "string" && EMAIL_RE.test(value);
278
+ return typeof value === "string" && EMAIL_RE2.test(value);
72
279
  }
73
280
  function clampArray(value, max) {
74
281
  if (!Array.isArray(value)) return value;
@@ -110,14 +317,23 @@ async function submitApplication(db, chapter, fields, opts) {
110
317
  if (app.requireDisclaimerAck && !acked) {
111
318
  return { ok: false, error: "disclaimerAck is required" };
112
319
  }
113
- const id2 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
114
- const row = { id: id2, status: chapter.pipeline.initial, createdAt: opts.now };
320
+ const id4 = opts.submissionId ? await applicationIdForSubmission(opts.submissionId) : opts.newId();
321
+ const row = { id: id4, status: chapter.pipeline.initial, createdAt: opts.now };
115
322
  for (const f of [...app.required, ...app.optional]) {
116
323
  if (fieldStates[f]?.visible === false) continue;
117
324
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
118
325
  }
119
326
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
120
327
  if (opts.groupId) row.groupId = opts.groupId;
328
+ if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
329
+ row.emailNormalized = normalizeAdmissionEmail(fields.email);
330
+ }
331
+ if (opts.admissionGrant?.decision === "waive_interview") {
332
+ row.admissionGrantId = opts.admissionGrant.id;
333
+ row.admissionSource = opts.admissionGrant.sourceScope;
334
+ row.interviewWaivedAt = opts.now;
335
+ }
336
+ if (opts.giftOfferId) row.giftOfferId = opts.giftOfferId;
121
337
  if (typeof fields.tierId === "string" && fields.tierId) {
122
338
  if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
123
339
  return { ok: false, error: "tierId is not an offered tier" };
@@ -125,11 +341,20 @@ async function submitApplication(db, chapter, fields, opts) {
125
341
  row.tierId = fields.tierId;
126
342
  }
127
343
  if (acked) row.disclaimerAckAt = opts.now;
344
+ const ops = [
345
+ { t: "update", ns: "applications", id: id4, attrs: row }
346
+ ];
347
+ if (opts.admissionGrant?.decision === "waive_interview") {
348
+ ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
349
+ consumedAt: opts.now,
350
+ consumedApplicationId: id4
351
+ } });
352
+ }
128
353
  const { duplicate } = await db.transact(
129
- [{ t: "update", ns: "applications", id: id2, attrs: row }],
354
+ ops,
130
355
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
131
356
  );
132
- return { ok: true, id: id2, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
357
+ return { ok: true, id: id4, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
133
358
  }
134
359
  function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
135
360
  return {
@@ -149,93 +374,13 @@ function joinConfig(group, paymentsReady2, tiers2 = [], conditions = {}) {
149
374
  };
150
375
  }
151
376
 
152
- // src/schema.ts
153
- function attr(type, flags = {}) {
154
- return {
155
- type,
156
- unique: flags.unique ?? false,
157
- indexed: flags.indexed ?? false,
158
- optional: flags.optional ?? false
159
- };
160
- }
161
- var id = () => attr("string", { unique: true, indexed: true });
162
- var admins = {
163
- attrs: {
164
- id: id(),
165
- email: attr("string", { unique: true, indexed: true }),
166
- name: attr("string", { optional: true }),
167
- note: attr("string", { optional: true })
168
- }
169
- };
170
- var superAdmins = {
171
- attrs: {
172
- id: id(),
173
- email: attr("string", { unique: true, indexed: true }),
174
- note: attr("string", { optional: true }),
175
- createdAt: attr("number", { indexed: true })
176
- }
177
- };
178
- var networkNotes = {
179
- attrs: {
180
- id: id(),
181
- recordKey: attr("string", { indexed: true }),
182
- targetId: attr("string", { indexed: true }),
183
- recordType: attr("string", { indexed: true }),
184
- recordId: attr("string", { indexed: true }),
185
- body: attr("string"),
186
- authorId: attr("string", { indexed: true }),
187
- authorEmail: attr("string", { optional: true }),
188
- createdAt: attr("number", { indexed: true })
189
- }
190
- };
191
- var signupControls = {
192
- attrs: {
193
- id: id(),
194
- sourceId: attr("string", { indexed: true }),
195
- targetId: attr("string", { indexed: true }),
196
- environment: attr("string", { indexed: true }),
197
- revision: attr("number", { indexed: true }),
198
- digest: attr("string", { indexed: true }),
199
- document: attr("json"),
200
- active: attr("boolean", { indexed: true }),
201
- receivedAt: attr("number", { indexed: true })
202
- }
203
- };
204
- var applications = {
205
- attrs: {
206
- id: id(),
207
- firstName: attr("string"),
208
- lastName: attr("string"),
209
- email: attr("string", { indexed: true }),
210
- referral: attr("string"),
211
- referralName: attr("string", { optional: true }),
212
- whoYouAre: attr("string"),
213
- focus: attr("json"),
214
- linkedin: attr("string", { optional: true }),
215
- message: attr("string"),
216
- status: attr("string", { indexed: true }),
217
- createdAt: attr("number", { indexed: true }),
218
- meetingAt: attr("number", { indexed: true, optional: true }),
219
- meetingLink: attr("string", { optional: true }),
220
- clerkUserId: attr("string", { indexed: true, optional: true }),
221
- // One-time/effect-repair marker. Absent means /api/me should copy the
222
- // allowlisted application snapshot into Clerk private_metadata and remove
223
- // the former public_metadata copy.
224
- clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
225
- phone: attr("string", { optional: true }),
226
- state: attr("string", { optional: true }),
227
- groupId: attr("string", { indexed: true, optional: true }),
228
- // The tier this application joined on. Absent on rows that predate tiers.
229
- tierId: attr("string", { indexed: true, optional: true }),
230
- stripeCustomerId: attr("string", { indexed: true, optional: true }),
231
- stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
232
- renewalAt: attr("number", { optional: true }),
233
- disclaimerAckAt: attr("number", { optional: true }),
234
- refundPolicyAckAt: attr("number", { optional: true }),
235
- prepEmailSentAt: attr("number", { optional: true }),
236
- canceled: attr("boolean", { optional: true })
237
- }
238
- };
377
+ // src/schema-applications.ts
378
+ var attr = (type, flags = {}) => ({
379
+ type,
380
+ unique: flags.unique ?? false,
381
+ indexed: flags.indexed ?? false,
382
+ optional: flags.optional ?? false
383
+ });
239
384
  var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
240
385
  "firstName",
241
386
  "lastName",
@@ -249,25 +394,57 @@ var APPLICATION_INPUT_FIELDS = /* @__PURE__ */ new Set([
249
394
  "phone",
250
395
  "state"
251
396
  ]);
397
+ var applicationAttrs = {
398
+ id: attr("string", { unique: true, indexed: true }),
399
+ firstName: attr("string"),
400
+ lastName: attr("string"),
401
+ email: attr("string", { indexed: true }),
402
+ referral: attr("string"),
403
+ referralName: attr("string", { optional: true }),
404
+ whoYouAre: attr("string"),
405
+ focus: attr("json"),
406
+ linkedin: attr("string", { optional: true }),
407
+ message: attr("string"),
408
+ status: attr("string", { indexed: true }),
409
+ createdAt: attr("number", { indexed: true }),
410
+ meetingAt: attr("number", { indexed: true, optional: true }),
411
+ meetingLink: attr("string", { optional: true }),
412
+ clerkUserId: attr("string", { indexed: true, optional: true }),
413
+ clerkPrivateMetadataSyncedAt: attr("number", { optional: true }),
414
+ phone: attr("string", { optional: true }),
415
+ state: attr("string", { optional: true }),
416
+ groupId: attr("string", { indexed: true, optional: true }),
417
+ tierId: attr("string", { indexed: true, optional: true }),
418
+ stripeCustomerId: attr("string", { indexed: true, optional: true }),
419
+ stripeSubscriptionId: attr("string", { indexed: true, optional: true }),
420
+ renewalAt: attr("number", { optional: true }),
421
+ disclaimerAckAt: attr("number", { optional: true }),
422
+ refundPolicyAckAt: attr("number", { optional: true }),
423
+ prepEmailSentAt: attr("number", { optional: true }),
424
+ canceled: attr("boolean", { optional: true }),
425
+ emailNormalized: attr("string", { indexed: true, optional: true }),
426
+ admissionGrantId: attr("string", { unique: true, indexed: true, optional: true }),
427
+ admissionSource: attr("string", { indexed: true, optional: true }),
428
+ interviewWaivedAt: attr("number", { optional: true }),
429
+ giftOfferId: attr("string", { optional: true, indexed: true }),
430
+ giftIntentId: attr("string", { optional: true, unique: true, indexed: true }),
431
+ giftClaimId: attr("string", { optional: true, unique: true, indexed: true }),
432
+ giftOrderId: attr("string", { optional: true, indexed: true }),
433
+ giftPaidAt: attr("number", { optional: true, indexed: true }),
434
+ giftRevokedAt: attr("number", { optional: true, indexed: true }),
435
+ giftCheckoutStartedAt: attr("number", { optional: true, indexed: true })
436
+ };
252
437
  function applicationsFor(application) {
253
438
  const attrs = {};
254
- for (const [name, spec] of Object.entries(applications.attrs)) {
255
- attrs[name] = {
256
- ...spec,
257
- optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional
258
- };
439
+ for (const [name, spec] of Object.entries(applicationAttrs)) {
440
+ attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
259
441
  }
260
442
  const apply = (field, optional) => {
261
443
  const existing = Object.hasOwn(attrs, field) ? attrs[field] : void 0;
262
- if (existing && !APPLICATION_INPUT_FIELDS.has(field)) {
263
- throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
264
- }
265
- if (existing && existing.type !== "string") {
266
- throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
267
- }
268
- const spec = existing ? { ...existing, optional } : attr("string", { optional });
444
+ if (existing && !APPLICATION_INPUT_FIELDS.has(field)) throw new Error(`defineChapter.application: field "${field}" is reserved by the applications schema`);
445
+ if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field}" is not a configurable string field`);
269
446
  Object.defineProperty(attrs, field, {
270
- value: spec,
447
+ value: existing ? { ...existing, optional } : attr("string", { optional }),
271
448
  enumerable: true,
272
449
  configurable: true,
273
450
  writable: true
@@ -277,75 +454,270 @@ function applicationsFor(application) {
277
454
  for (const field of application.optional) apply(field, true);
278
455
  return { attrs };
279
456
  }
280
- var groups = {
457
+
458
+ // src/schema-admission.ts
459
+ var attr2 = (type, flags = {}) => ({
460
+ type,
461
+ unique: flags.unique ?? false,
462
+ indexed: flags.indexed ?? false,
463
+ optional: flags.optional ?? false
464
+ });
465
+ var id = () => attr2("string", { unique: true, indexed: true });
466
+ var admissionGrantsEntity = {
281
467
  attrs: {
282
468
  id: id(),
283
- name: attr("string"),
284
- standardPriceCents: attr("number"),
285
- foundingDiscountCents: attr("number"),
286
- stripePriceId: attr("string", { optional: true }),
287
- stripePublishableKey: attr("string", { optional: true }),
288
- notificationEmail: attr("string"),
289
- replyTo: attr("string"),
290
- debugEmail: attr("string", { optional: true }),
291
- calendarLink: attr("string", { optional: true }),
292
- disclaimerText: attr("string"),
293
- refundPolicyText: attr("string"),
294
- trustCopy: attr("string"),
295
- commitmentText: attr("string", { optional: true }),
296
- normsText: attr("string", { optional: true }),
297
- emailTemplates: attr("json"),
298
- schedulingJson: attr("json", { optional: true }),
299
- createdAt: attr("number", { indexed: true })
469
+ sourceGrantId: attr2("string", { indexed: true }),
470
+ sourceScope: attr2("string", { indexed: true }),
471
+ sourceId: attr2("string", { indexed: true }),
472
+ targetId: attr2("string", { indexed: true }),
473
+ email: attr2("string"),
474
+ emailNormalized: attr2("string", { indexed: true }),
475
+ decision: attr2("string", { indexed: true }),
476
+ note: attr2("string", { optional: true }),
477
+ active: attr2("boolean", { indexed: true }),
478
+ sourceRevision: attr2("number", { optional: true, indexed: true }),
479
+ createdAt: attr2("number", { indexed: true }),
480
+ createdBy: attr2("string", { optional: true, indexed: true }),
481
+ updatedAt: attr2("number", { optional: true, indexed: true }),
482
+ updatedBy: attr2("string", { optional: true, indexed: true }),
483
+ expiresAt: attr2("number", { optional: true, indexed: true }),
484
+ revokedAt: attr2("number", { optional: true, indexed: true }),
485
+ revokedBy: attr2("string", { optional: true, indexed: true }),
486
+ consumedAt: attr2("number", { optional: true, indexed: true }),
487
+ consumedApplicationId: attr2("string", { optional: true, indexed: true })
300
488
  }
301
489
  };
302
- var tiers = {
490
+ var admissionGrantRevisionsEntity = {
303
491
  attrs: {
304
492
  id: id(),
305
- groupId: attr("string", { indexed: true }),
306
- name: attr("string"),
307
- priceCents: attr("number"),
308
- stripePriceId: attr("string", { optional: true }),
309
- blurb: attr("string", { optional: true }),
310
- sortOrder: attr("number", { indexed: true }),
311
- active: attr("boolean", { indexed: true }),
312
- createdAt: attr("number", { indexed: true })
493
+ sourceId: attr2("string", { indexed: true }),
494
+ targetId: attr2("string", { indexed: true }),
495
+ environment: attr2("string", { indexed: true }),
496
+ revision: attr2("number", { indexed: true }),
497
+ digest: attr2("string", { indexed: true }),
498
+ active: attr2("boolean", { indexed: true }),
499
+ receivedAt: attr2("number", { indexed: true })
500
+ }
501
+ };
502
+
503
+ // src/schema-gifts.ts
504
+ var attr3 = (type, flags = {}) => ({
505
+ type,
506
+ unique: flags.unique ?? false,
507
+ indexed: flags.indexed ?? false,
508
+ optional: flags.optional ?? false
509
+ });
510
+ var id2 = () => attr3("string", { unique: true, indexed: true });
511
+ var giftPurchaseIntentsEntity = { attrs: {
512
+ id: id2(),
513
+ chapterId: attr3("string", { indexed: true }),
514
+ environment: attr3("string", { indexed: true }),
515
+ purchaserType: attr3("string", { indexed: true }),
516
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
517
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
518
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
519
+ offerId: attr3("string", { indexed: true }),
520
+ offerSnapshot: attr3("json"),
521
+ status: attr3("string", { indexed: true }),
522
+ codeHash: attr3("string", { indexed: true }),
523
+ codeVersion: attr3("number"),
524
+ codeIssuedAt: attr3("number"),
525
+ providerObjectId: attr3("string", { optional: true, unique: true, indexed: true }),
526
+ providerCustomerId: attr3("string", { optional: true, indexed: true }),
527
+ checkoutStartedAt: attr3("number", { optional: true }),
528
+ paidAt: attr3("number", { optional: true, indexed: true }),
529
+ claimId: attr3("string", { optional: true, unique: true, indexed: true }),
530
+ createdAt: attr3("number", { indexed: true }),
531
+ updatedAt: attr3("number", { indexed: true })
532
+ } };
533
+ var giftOrdersEntity = { attrs: {
534
+ id: id2(),
535
+ intentId: attr3("string", { unique: true, indexed: true }),
536
+ chapterId: attr3("string", { indexed: true }),
537
+ environment: attr3("string", { indexed: true }),
538
+ offerId: attr3("string", { indexed: true }),
539
+ offerSnapshot: attr3("json"),
540
+ purchaserType: attr3("string", { indexed: true }),
541
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
542
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
543
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
544
+ providerEventId: attr3("string", { unique: true, indexed: true }),
545
+ providerObjectId: attr3("string", { indexed: true }),
546
+ amountCents: attr3("number"),
547
+ currency: attr3("string"),
548
+ billingMode: attr3("string"),
549
+ createdAt: attr3("number", { indexed: true })
550
+ } };
551
+ var giftClaimsEntity = { attrs: {
552
+ id: id2(),
553
+ orderId: attr3("string", { unique: true, indexed: true }),
554
+ intentId: attr3("string", { unique: true, indexed: true }),
555
+ chapterId: attr3("string", { indexed: true }),
556
+ offerId: attr3("string", { indexed: true }),
557
+ target: attr3("json"),
558
+ purchaserApplicationId: attr3("string", { optional: true, indexed: true }),
559
+ purchaserUserId: attr3("string", { optional: true, indexed: true }),
560
+ purchaserEmailNormalized: attr3("string", { indexed: true }),
561
+ codeHash: attr3("string", { unique: true, indexed: true }),
562
+ codeVersion: attr3("number"),
563
+ rotationCount: attr3("number"),
564
+ codeIssuedAt: attr3("number"),
565
+ codeExpiresAt: attr3("number", { indexed: true }),
566
+ attemptCount: attr3("number"),
567
+ attemptWindowStartedAt: attr3("number"),
568
+ status: attr3("string", { indexed: true }),
569
+ redeemedAt: attr3("number", { optional: true, indexed: true }),
570
+ beneficiaryEmailNormalized: attr3("string", { optional: true, indexed: true }),
571
+ beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
572
+ applicationId: attr3("string", { optional: true, unique: true, indexed: true }),
573
+ entitlementId: attr3("string", { optional: true, unique: true, indexed: true }),
574
+ rotatedAt: attr3("number", { optional: true }),
575
+ revokedAt: attr3("number", { optional: true, indexed: true }),
576
+ revokedReason: attr3("string", { optional: true }),
577
+ createdAt: attr3("number", { indexed: true })
578
+ } };
579
+ var giftEntitlementsEntity = { attrs: {
580
+ id: id2(),
581
+ claimId: attr3("string", { unique: true, indexed: true }),
582
+ orderId: attr3("string", { indexed: true }),
583
+ chapterId: attr3("string", { indexed: true }),
584
+ entitlementKey: attr3("string", { indexed: true }),
585
+ beneficiaryEmailNormalized: attr3("string", { indexed: true }),
586
+ beneficiaryUserId: attr3("string", { optional: true, indexed: true }),
587
+ active: attr3("boolean", { indexed: true }),
588
+ createdAt: attr3("number", { indexed: true }),
589
+ revokedAt: attr3("number", { optional: true })
590
+ } };
591
+ var giftRedemptionAttemptsEntity = { attrs: {
592
+ id: id2(),
593
+ chapterId: attr3("string", { indexed: true }),
594
+ bucket: attr3("string", { indexed: true }),
595
+ attemptCount: attr3("number"),
596
+ windowStartedAt: attr3("number", { indexed: true }),
597
+ updatedAt: attr3("number")
598
+ } };
599
+
600
+ // src/schema.ts
601
+ function attr4(type, flags = {}) {
602
+ return {
603
+ type,
604
+ unique: flags.unique ?? false,
605
+ indexed: flags.indexed ?? false,
606
+ optional: flags.optional ?? false
607
+ };
608
+ }
609
+ var id3 = () => attr4("string", { unique: true, indexed: true });
610
+ var admins = {
611
+ attrs: {
612
+ id: id3(),
613
+ email: attr4("string", { unique: true, indexed: true }),
614
+ name: attr4("string", { optional: true }),
615
+ note: attr4("string", { optional: true })
616
+ }
617
+ };
618
+ var superAdmins = {
619
+ attrs: {
620
+ id: id3(),
621
+ email: attr4("string", { unique: true, indexed: true }),
622
+ note: attr4("string", { optional: true }),
623
+ createdAt: attr4("number", { indexed: true })
624
+ }
625
+ };
626
+ var networkNotes = {
627
+ attrs: {
628
+ id: id3(),
629
+ recordKey: attr4("string", { indexed: true }),
630
+ targetId: attr4("string", { indexed: true }),
631
+ recordType: attr4("string", { indexed: true }),
632
+ recordId: attr4("string", { indexed: true }),
633
+ body: attr4("string"),
634
+ authorId: attr4("string", { indexed: true }),
635
+ authorEmail: attr4("string", { optional: true }),
636
+ createdAt: attr4("number", { indexed: true })
637
+ }
638
+ };
639
+ var signupControls = {
640
+ attrs: {
641
+ id: id3(),
642
+ sourceId: attr4("string", { indexed: true }),
643
+ targetId: attr4("string", { indexed: true }),
644
+ environment: attr4("string", { indexed: true }),
645
+ revision: attr4("number", { indexed: true }),
646
+ digest: attr4("string", { indexed: true }),
647
+ document: attr4("json"),
648
+ active: attr4("boolean", { indexed: true }),
649
+ receivedAt: attr4("number", { indexed: true })
650
+ }
651
+ };
652
+ var groups = {
653
+ attrs: {
654
+ id: id3(),
655
+ name: attr4("string"),
656
+ standardPriceCents: attr4("number"),
657
+ foundingDiscountCents: attr4("number"),
658
+ stripePriceId: attr4("string", { optional: true }),
659
+ stripePublishableKey: attr4("string", { optional: true }),
660
+ notificationEmail: attr4("string"),
661
+ replyTo: attr4("string"),
662
+ debugEmail: attr4("string", { optional: true }),
663
+ calendarLink: attr4("string", { optional: true }),
664
+ disclaimerText: attr4("string"),
665
+ refundPolicyText: attr4("string"),
666
+ trustCopy: attr4("string"),
667
+ commitmentText: attr4("string", { optional: true }),
668
+ normsText: attr4("string", { optional: true }),
669
+ emailTemplates: attr4("json"),
670
+ schedulingJson: attr4("json", { optional: true }),
671
+ createdAt: attr4("number", { indexed: true })
672
+ }
673
+ };
674
+ var tiers = {
675
+ attrs: {
676
+ id: id3(),
677
+ groupId: attr4("string", { indexed: true }),
678
+ name: attr4("string"),
679
+ priceCents: attr4("number"),
680
+ stripePriceId: attr4("string", { optional: true }),
681
+ blurb: attr4("string", { optional: true }),
682
+ sortOrder: attr4("number", { indexed: true }),
683
+ active: attr4("boolean", { indexed: true }),
684
+ createdAt: attr4("number", { indexed: true })
313
685
  }
314
686
  };
315
687
  var meetings = {
316
688
  attrs: {
317
- id: id(),
318
- applicationId: attr("string", { indexed: true }),
319
- groupId: attr("string", { indexed: true }),
320
- startAt: attr("number", { indexed: true }),
321
- endAt: attr("number"),
322
- timezone: attr("string"),
323
- status: attr("string", { indexed: true }),
324
- googleEventId: attr("string", { indexed: true, optional: true }),
325
- meetUrl: attr("string", { optional: true }),
326
- htmlLink: attr("string", { optional: true }),
327
- drift: attr("string", { indexed: true, optional: true }),
328
- driftGoogleStartAt: attr("number", { optional: true }),
329
- driftDetectedAt: attr("number", { optional: true }),
330
- adoptedFromGoogleAt: attr("number", { optional: true }),
331
- createdAt: attr("number", { indexed: true })
689
+ id: id3(),
690
+ applicationId: attr4("string", { indexed: true }),
691
+ groupId: attr4("string", { indexed: true }),
692
+ startAt: attr4("number", { indexed: true }),
693
+ endAt: attr4("number"),
694
+ timezone: attr4("string"),
695
+ status: attr4("string", { indexed: true }),
696
+ googleEventId: attr4("string", { indexed: true, optional: true }),
697
+ meetUrl: attr4("string", { optional: true }),
698
+ htmlLink: attr4("string", { optional: true }),
699
+ drift: attr4("string", { indexed: true, optional: true }),
700
+ driftGoogleStartAt: attr4("number", { optional: true }),
701
+ driftDetectedAt: attr4("number", { optional: true }),
702
+ adoptedFromGoogleAt: attr4("number", { optional: true }),
703
+ createdAt: attr4("number", { indexed: true })
332
704
  }
333
705
  };
334
706
  var emailLog = {
335
707
  attrs: {
336
- id: id(),
337
- groupId: attr("string", { indexed: true }),
338
- applicationId: attr("string", { indexed: true, optional: true }),
339
- to: attr("string", { indexed: true }),
340
- template: attr("string", { indexed: true }),
341
- subject: attr("string"),
342
- body: attr("string", { optional: true }),
343
- transport: attr("string"),
344
- messageId: attr("string", { optional: true }),
345
- redirected: attr("boolean", { optional: true }),
346
- dedupeKey: attr("string", { indexed: true, optional: true }),
347
- error: attr("string", { optional: true }),
348
- sentAt: attr("number", { indexed: true })
708
+ id: id3(),
709
+ groupId: attr4("string", { indexed: true }),
710
+ applicationId: attr4("string", { indexed: true, optional: true }),
711
+ to: attr4("string", { indexed: true }),
712
+ template: attr4("string", { indexed: true }),
713
+ subject: attr4("string"),
714
+ body: attr4("string", { optional: true }),
715
+ transport: attr4("string"),
716
+ messageId: attr4("string", { optional: true }),
717
+ redirected: attr4("boolean", { optional: true }),
718
+ dedupeKey: attr4("string", { indexed: true, optional: true }),
719
+ error: attr4("string", { optional: true }),
720
+ sentAt: attr4("number", { indexed: true })
349
721
  }
350
722
  };
351
723
  function chapterDb(mode, auth, includeNetworkNotes = false, application = resolveApplication(void 0), includeSignupControl = false) {
@@ -356,6 +728,13 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
356
728
  entities.tiers = tiers;
357
729
  entities.meetings = meetings;
358
730
  entities.emailLog = emailLog;
731
+ entities.admissionGrants = admissionGrantsEntity;
732
+ entities.admissionGrantRevisions = admissionGrantRevisionsEntity;
733
+ entities.giftPurchaseIntents = giftPurchaseIntentsEntity;
734
+ entities.giftOrders = giftOrdersEntity;
735
+ entities.giftClaims = giftClaimsEntity;
736
+ entities.giftEntitlements = giftEntitlementsEntity;
737
+ entities.giftRedemptionAttempts = giftRedemptionAttemptsEntity;
359
738
  if (includeSignupControl) entities.signupControls = signupControls;
360
739
  }
361
740
  if (auth.source === "table") entities.admins = admins;
@@ -640,6 +1019,7 @@ var DEFAULT_ADMIN_COPY = {
640
1019
  overview: "Overview",
641
1020
  billing: "Billing",
642
1021
  people: "People",
1022
+ admission: "Admission",
643
1023
  network: "Network",
644
1024
  portfolio: "Portfolio",
645
1025
  settings: "Settings",
@@ -750,6 +1130,26 @@ var DEFAULT_ADMIN_COPY = {
750
1130
  reschedule: "Reschedule",
751
1131
  cancel: "Cancel"
752
1132
  },
1133
+ admission: {
1134
+ title: "Interview decisions",
1135
+ description: "Local decisions override Parent decisions. Requiring an interview locally suppresses a Parent waiver for the same email.",
1136
+ email: "Email",
1137
+ decision: "Decision",
1138
+ expires: "Expires",
1139
+ note: "Internal note",
1140
+ waive: "Waive interview",
1141
+ require: "Require interview",
1142
+ add: "Add decision",
1143
+ ledger: "Effective grant ledger",
1144
+ loading: "Loading\u2026",
1145
+ consumed: "consumed",
1146
+ revoked: "revoked",
1147
+ expired: "expired",
1148
+ active: "active",
1149
+ local: "local",
1150
+ parent: "Parent",
1151
+ revoke: "Revoke"
1152
+ },
753
1153
  network: {
754
1154
  title: "Share to a follower",
755
1155
  allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
@@ -848,6 +1248,19 @@ var DEFAULT_JOIN_COPY = {
848
1248
  label: "You're booked",
849
1249
  calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
850
1250
  memberArea: "Go to your member area"
1251
+ },
1252
+ accepted: {
1253
+ label: "You're approved",
1254
+ body: "Your membership is approved. No introduction call is required."
1255
+ },
1256
+ gift: {
1257
+ chooseSelf: "Buy membership for myself",
1258
+ optionsLabel: "Or give access to someone else",
1259
+ label: "Your gift code",
1260
+ body: "Share this one-time code with the recipient. It becomes active after payment is confirmed.",
1261
+ save: "Save it now. For security, this code is shown only once.",
1262
+ rotate: "Generate a new code",
1263
+ rotateFailed: "A new code could not be generated."
851
1264
  }
852
1265
  };
853
1266
  var DEFAULT_MEMBERS_COPY = {
@@ -875,6 +1288,26 @@ var DEFAULT_MEMBERS_COPY = {
875
1288
  full: {
876
1289
  welcome: "Welcome back."
877
1290
  },
1291
+ gifts: {
1292
+ title: "Give access",
1293
+ body: "Purchase access for someone else and share the one-time claim code.",
1294
+ noneAvailable: "No gift options are available right now.",
1295
+ paid: "Payment submitted. Save the code below while it is visible.",
1296
+ saveCode: "This code is shown once. Save it before leaving this page.",
1297
+ purchases: "Your gifts",
1298
+ noPurchases: "No gift purchases yet.",
1299
+ pending: "Pending",
1300
+ redeemed: "Redeemed",
1301
+ expired: "Expired",
1302
+ revoked: "Revoked",
1303
+ rotate: "Rotate code",
1304
+ rotated: "A new code was generated. Save it now.",
1305
+ rotateFailed: "The code could not be rotated.",
1306
+ redeemTitle: "Redeem a gift",
1307
+ code: "One-time code",
1308
+ redeem: "Redeem",
1309
+ redeemFailed: "The code is invalid or unavailable."
1310
+ },
878
1311
  reschedule: {
879
1312
  changeTime: "Change your time",
880
1313
  unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
@@ -990,13 +1423,13 @@ function resolveLeaderFormation(input, crm) {
990
1423
  }
991
1424
  function formationFields(crm, formation) {
992
1425
  const def = crm.type(formation.type);
993
- return [...formation.required, ...formation.optional].map((id2) => {
994
- const field = def.fields[id2];
1426
+ return [...formation.required, ...formation.optional].map((id4) => {
1427
+ const field = def.fields[id4];
995
1428
  return {
996
- id: id2,
997
- label: field.label ?? id2,
1429
+ id: id4,
1430
+ label: field.label ?? id4,
998
1431
  type: field.type,
999
- required: formation.required.includes(id2),
1432
+ required: formation.required.includes(id4),
1000
1433
  ...field.options ? { options: field.options } : {}
1001
1434
  };
1002
1435
  });
@@ -1155,9 +1588,9 @@ function isResolvedCrm(x) {
1155
1588
  }
1156
1589
  function defineChapter(config) {
1157
1590
  if (!config || typeof config !== "object") throw new Error("defineChapter: a config object is required");
1158
- const { id: id2, name } = config;
1159
- if (typeof id2 !== "string" || !SLUG3.test(id2)) {
1160
- throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id2)}`);
1591
+ const { id: id4, name } = config;
1592
+ if (typeof id4 !== "string" || !SLUG3.test(id4)) {
1593
+ throw new Error(`defineChapter.id: must be a lowercase slug [a-z0-9-] (2-63 chars) \u2014 got ${JSON.stringify(id4)}`);
1161
1594
  }
1162
1595
  if (typeof name !== "string" || name.trim() === "") throw new Error("defineChapter.name: a non-empty name is required");
1163
1596
  const mode = config.mode ?? "chapter";
@@ -1240,7 +1673,7 @@ function defineChapter(config) {
1240
1673
  };
1241
1674
  const chapter = {
1242
1675
  config,
1243
- id: id2,
1676
+ id: id4,
1244
1677
  name,
1245
1678
  mode,
1246
1679
  brand,
@@ -1454,8 +1887,8 @@ function tierPayable(tier, group, hasSecretKey) {
1454
1887
  function byOrder(a, b) {
1455
1888
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
1456
1889
  }
1457
- function resolveTiers(group, rows = []) {
1458
- const stored = rows.filter((tier) => tier.active);
1890
+ function resolveTiers(group, rows2 = []) {
1891
+ const stored = rows2.filter((tier) => tier.active);
1459
1892
  if (stored.length) return [...stored].sort(byOrder);
1460
1893
  return [{
1461
1894
  id: "standard",
@@ -1577,10 +2010,10 @@ function planDelivery(input) {
1577
2010
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
1578
2011
  const to = redirect ? input.group.debugEmail : input.to;
1579
2012
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
1580
- const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
2013
+ const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
1581
2014
 
1582
2015
  ` + render(tpl.text, vars) : render(tpl.text, vars);
1583
- return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
2016
+ return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
1584
2017
  }
1585
2018
 
1586
2019
  // src/notify.ts
@@ -1615,9 +2048,9 @@ async function sendTemplated(deps, input) {
1615
2048
  error = e instanceof Error ? e.message : String(e);
1616
2049
  }
1617
2050
  }
1618
- const id2 = deps.newId();
2051
+ const id4 = deps.newId();
1619
2052
  const row = {
1620
- id: id2,
2053
+ id: id4,
1621
2054
  groupId: input.group.id,
1622
2055
  to: decision.to,
1623
2056
  template: input.template,
@@ -1631,7 +2064,7 @@ async function sendTemplated(deps, input) {
1631
2064
  ...messageId ? { messageId } : {},
1632
2065
  ...error ? { error } : {}
1633
2066
  };
1634
- await deps.db.transact([{ t: "update", ns: "emailLog", id: id2, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
2067
+ await deps.db.transact([{ t: "update", ns: "emailLog", id: id4, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
1635
2068
  return error ? { sent: false, reason: error } : { sent: true };
1636
2069
  }
1637
2070
  function emailGroupFrom(row) {
@@ -1667,8 +2100,26 @@ function findApplicationRef(obj) {
1667
2100
  const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
1668
2101
  return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
1669
2102
  }
2103
+ function findGiftRef(obj) {
2104
+ const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
2105
+ const pick = (metadata) => typeof metadata.giftIntentId === "string" ? metadata.giftIntentId : void 0;
2106
+ const parent = obj.parent;
2107
+ const subscription = obj.subscription_details ?? parent?.subscription_details;
2108
+ const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
2109
+ const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
2110
+ return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
2111
+ }
1670
2112
  function normalizeWebhookEvent(event) {
1671
2113
  const obj = event.data?.object ?? {};
2114
+ const gift = findGiftRef(obj);
2115
+ if (gift.giftIntentId) {
2116
+ if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
2117
+ return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
2118
+ }
2119
+ if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
2120
+ if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
2121
+ return { kind: "ignored", type: event.type };
2122
+ }
1672
2123
  const ref = findApplicationRef(obj);
1673
2124
  switch (event.type) {
1674
2125
  case "invoice.paid": {
@@ -1702,6 +2153,112 @@ function canceledPatch() {
1702
2153
  return { canceled: true };
1703
2154
  }
1704
2155
 
2156
+ // src/join-continuation.ts
2157
+ function resolveJoinContinuation(input) {
2158
+ if (input.status === "approved") return "accepted";
2159
+ if (input.interviewWaived) {
2160
+ if (!input.payable) return "approve";
2161
+ if (input.status === input.initialStatus) {
2162
+ return input.subscriptionStarted ? "paymentPending" : "payment";
2163
+ }
2164
+ return "approve";
2165
+ }
2166
+ if (!input.payable) return "booking";
2167
+ if (input.status === input.initialStatus) {
2168
+ return input.subscriptionStarted ? "paymentPending" : "payment";
2169
+ }
2170
+ if (canBook(input.status, input.pipeline)) return "booking";
2171
+ return null;
2172
+ }
2173
+
2174
+ // src/gift-offers.ts
2175
+ var GIFT_OFFER_CATALOG_VERSION = 1;
2176
+ var MAX_GIFT_OFFERS = 20;
2177
+ var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
2178
+ var PRICE = /^price_[A-Za-z0-9]{12,}$/;
2179
+ var CURRENCY = /^[a-z]{3}$/;
2180
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
2181
+ var text = (value, path, errors, max) => {
2182
+ if (typeof value !== "string" || !value.trim()) {
2183
+ errors.push(`${path} is required`);
2184
+ return "";
2185
+ }
2186
+ if (value.trim().length > max) errors.push(`${path} must be at most ${max} characters`);
2187
+ return value.trim();
2188
+ };
2189
+ function validateGiftOfferCatalog(input, tierIds) {
2190
+ const errors = [];
2191
+ const root = record(input);
2192
+ if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
2193
+ for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
2194
+ if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
2195
+ if (!Array.isArray(root.offers)) errors.push("document.giftOffers.offers must be an array");
2196
+ const rawOffers = Array.isArray(root.offers) ? root.offers : [];
2197
+ if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
2198
+ const ids = /* @__PURE__ */ new Set();
2199
+ const offers = [];
2200
+ for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
2201
+ const path = `document.giftOffers.offers[${index}]`;
2202
+ const item = record(raw);
2203
+ if (!item) {
2204
+ errors.push(`${path} must be an object`);
2205
+ continue;
2206
+ }
2207
+ const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
2208
+ for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2209
+ const id4 = text(item.id, `${path}.id`, errors, 80);
2210
+ if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2211
+ if (ids.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
2212
+ ids.add(id4);
2213
+ const targetRaw = record(item.target);
2214
+ let target = { type: "entitlement", entitlementKey: "invalid" };
2215
+ if (targetRaw?.type === "tier") {
2216
+ const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
2217
+ if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
2218
+ target = { type: "tier", tierId };
2219
+ } else if (targetRaw?.type === "entitlement") {
2220
+ const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
2221
+ if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
2222
+ target = { type: "entitlement", entitlementKey };
2223
+ } else errors.push(`${path}.target.type must be tier or entitlement`);
2224
+ if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
2225
+ const stripePriceId = text(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
2226
+ if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2227
+ 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`);
2228
+ const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
2229
+ if (!CURRENCY.test(currency)) errors.push(`${path}.currency must be a lowercase ISO currency code`);
2230
+ const lifetime = Number(item.redemptionLifetimeHours);
2231
+ if (!Number.isSafeInteger(lifetime) || lifetime < 1 || lifetime > 8760) errors.push(`${path}.redemptionLifetimeHours must be an integer from 1 to 8760`);
2232
+ const activeFrom = item.activeFrom === void 0 ? void 0 : Number(item.activeFrom);
2233
+ const activeUntil = item.activeUntil === void 0 ? void 0 : Number(item.activeUntil);
2234
+ if (activeFrom !== void 0 && (!Number.isSafeInteger(activeFrom) || activeFrom < 1)) errors.push(`${path}.activeFrom must be epoch milliseconds`);
2235
+ if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
2236
+ if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
2237
+ offers.push({
2238
+ id: id4,
2239
+ target,
2240
+ billingMode: item.billingMode,
2241
+ stripePriceId,
2242
+ priceCents: Number(item.priceCents),
2243
+ currency,
2244
+ redemptionLifetimeHours: lifetime,
2245
+ ...activeFrom !== void 0 ? { activeFrom } : {},
2246
+ ...activeUntil !== void 0 ? { activeUntil } : {},
2247
+ title: text(item.title, `${path}.title`, errors, 160),
2248
+ description: text(item.description, `${path}.description`, errors, 2e3),
2249
+ ctaLabel: text(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
2250
+ });
2251
+ }
2252
+ return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
2253
+ }
2254
+ var giftOfferActive = (offer, now) => (!offer.activeFrom || offer.activeFrom <= now) && (!offer.activeUntil || offer.activeUntil > now);
2255
+ function publicGiftOffers(catalog, now) {
2256
+ return (catalog?.offers ?? []).filter((offer) => giftOfferActive(offer, now)).map(({ stripePriceId: _stripe, target: _target, ...offer }) => offer);
2257
+ }
2258
+ function findGiftOffer(catalog, offerId, now) {
2259
+ return catalog?.offers.find((offer) => offer.id === offerId && giftOfferActive(offer, now)) ?? null;
2260
+ }
2261
+
1705
2262
  // src/network.ts
1706
2263
  import {
1707
2264
  createRecord,
@@ -1746,34 +2303,34 @@ function networkSourceTag(type, hubRecordId, sourceId) {
1746
2303
  if (readable && raw.length <= 64) return raw;
1747
2304
  return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
1748
2305
  }
1749
- function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1750
- if ("input" in record && record.version === 2) {
2306
+ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
2307
+ if ("input" in record2 && record2.version === 2) {
1751
2308
  return {
1752
2309
  version: 2,
1753
- sourceId: record.source.siteId,
1754
- sourceRecordId: record.source.recordId,
1755
- type: record.type,
1756
- input: record.input
2310
+ sourceId: record2.source.siteId,
2311
+ sourceRecordId: record2.source.recordId,
2312
+ type: record2.type,
2313
+ input: record2.input
1757
2314
  };
1758
2315
  }
1759
- if ("input" in record) {
2316
+ if ("input" in record2) {
1760
2317
  return {
1761
2318
  version: 1,
1762
2319
  sourceId: fallbackSourceId,
1763
- sourceRecordId: record.hubRecordId,
1764
- type: record.type,
1765
- input: record.input
2320
+ sourceRecordId: record2.hubRecordId,
2321
+ type: record2.type,
2322
+ input: record2.input
1766
2323
  };
1767
2324
  }
1768
- if ("type" in record && record.type === "company") {
1769
- const input = { name: record.name };
2325
+ if ("type" in record2 && record2.type === "company") {
2326
+ const input = { name: record2.name };
1770
2327
  for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
1771
- if (record[key]) input[key] = record[key];
2328
+ if (record2[key]) input[key] = record2[key];
1772
2329
  }
1773
2330
  return {
1774
2331
  version: 1,
1775
2332
  sourceId: fallbackSourceId,
1776
- sourceRecordId: record.hubRecordId,
2333
+ sourceRecordId: record2.hubRecordId,
1777
2334
  type: "company",
1778
2335
  input
1779
2336
  };
@@ -1781,28 +2338,28 @@ function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
1781
2338
  return {
1782
2339
  version: 1,
1783
2340
  sourceId: fallbackSourceId,
1784
- sourceRecordId: record.hubRecordId,
2341
+ sourceRecordId: record2.hubRecordId,
1785
2342
  type: "person",
1786
- input: sharedPersonInput(record)
2343
+ input: sharedPersonInput(record2)
1787
2344
  };
1788
2345
  }
1789
- function sharedRecordFromCrm(crm, record, target, sourceId) {
1790
- if (target.fields && !target.fields[record.type]) {
1791
- throw new Error(`${target.name} does not accept "${record.type}" records`);
2346
+ function sharedRecordFromCrm(crm, record2, target, sourceId) {
2347
+ if (target.fields && !target.fields[record2.type]) {
2348
+ throw new Error(`${target.name} does not accept "${record2.type}" records`);
1792
2349
  }
1793
- const def = crm.type(record.type);
1794
- const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
2350
+ const def = crm.type(record2.type);
2351
+ const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
1795
2352
  if (!fields) {
1796
- throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
2353
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record2.type}" records`);
1797
2354
  }
1798
2355
  const nameField = def.nameField ?? "name";
1799
2356
  const input = {};
1800
2357
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
1801
- const value = record.fields?.[field];
2358
+ const value = record2.fields?.[field];
1802
2359
  if (value !== void 0) input[field] = value;
1803
2360
  }
1804
- if (input[nameField] === void 0) input[nameField] = record.name;
1805
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
2361
+ if (input[nameField] === void 0) input[nameField] = record2.name;
2362
+ return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
1806
2363
  }
1807
2364
  async function upsertPerson(deps, opts) {
1808
2365
  const email = opts.email.toLowerCase();
@@ -1816,11 +2373,11 @@ async function upsertPerson(deps, opts) {
1816
2373
  const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
1817
2374
  return { recordId: created.id };
1818
2375
  }
1819
- async function findSharedRecord(deps, record, tag) {
2376
+ async function findSharedRecord(deps, record2, tag) {
1820
2377
  const structured = await getRecordByOrigin(
1821
2378
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
1822
- record.sourceId,
1823
- record.sourceRecordId
2379
+ record2.sourceId,
2380
+ record2.sourceRecordId
1824
2381
  );
1825
2382
  if (structured) return structured;
1826
2383
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -1829,53 +2386,53 @@ async function findSharedRecord(deps, record, tag) {
1829
2386
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
1830
2387
  if (found.crm_record?.[0]) return found.crm_record[0];
1831
2388
  }
1832
- const def = deps.crm.type(record.type);
2389
+ const def = deps.crm.type(record2.type);
1833
2390
  const emailField = def.emailField;
1834
- if (emailField && typeof record.input[emailField] === "string") {
1835
- const primaryEmail = record.input[emailField].toLowerCase();
2391
+ if (emailField && typeof record2.input[emailField] === "string") {
2392
+ const primaryEmail = record2.input[emailField].toLowerCase();
1836
2393
  const found = await deps.db.query({
1837
- crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
2394
+ crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
1838
2395
  });
1839
2396
  if (found.crm_record?.[0]) return found.crm_record[0];
1840
2397
  }
1841
- const domain = record.input.domain;
2398
+ const domain = record2.input.domain;
1842
2399
  const domainSlot = def.fields.domain?.slot;
1843
2400
  if (typeof domain === "string" && domainSlot) {
1844
2401
  const found = await deps.db.query({
1845
- crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
2402
+ crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
1846
2403
  });
1847
2404
  if (found.crm_record?.[0]) return found.crm_record[0];
1848
2405
  }
1849
2406
  const nameField = def.nameField ?? "name";
1850
- const name = record.input[nameField];
1851
- if (record.type === "company" && typeof name === "string" && name.trim()) {
2407
+ const name = record2.input[nameField];
2408
+ if (record2.type === "company" && typeof name === "string" && name.trim()) {
1852
2409
  const found = await deps.db.query({
1853
- crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
2410
+ crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
1854
2411
  });
1855
2412
  if (found.crm_record?.[0]) return found.crm_record[0];
1856
2413
  }
1857
2414
  return void 0;
1858
2415
  }
1859
2416
  async function projectSharedRecord(deps, shared, options = {}) {
1860
- const record = normalizeSharedRecord(shared, options.sourceId);
1861
- if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
2417
+ const record2 = normalizeSharedRecord(shared, options.sourceId);
2418
+ if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
1862
2419
  throw new Error("type, source site id, and source record id must be non-empty");
1863
2420
  }
1864
- if (options.sourceId && options.sourceId !== record.sourceId) {
2421
+ if (options.sourceId && options.sourceId !== record2.sourceId) {
1865
2422
  throw new Error("signed sender does not match payload source");
1866
2423
  }
1867
- const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
2424
+ const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
1868
2425
  const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1869
- const existing = await findSharedRecord(deps, record, tag);
2426
+ const existing = await findSharedRecord(deps, record2, tag);
1870
2427
  let recordId;
1871
2428
  if (existing && typeof existing.id === "string") {
1872
- await updateRecord(crmDeps, { id: existing.id, input: record.input });
2429
+ await updateRecord(crmDeps, { id: existing.id, input: record2.input });
1873
2430
  recordId = existing.id;
1874
2431
  } else {
1875
- recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
2432
+ recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
1876
2433
  await createRecord({ ...crmDeps, newId: () => recordId }, {
1877
- type: record.type,
1878
- input: record.input,
2434
+ type: record2.type,
2435
+ input: record2.input,
1879
2436
  mutationId: `share-create:${tag}`
1880
2437
  });
1881
2438
  }
@@ -1885,10 +2442,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
1885
2442
  );
1886
2443
  await upsertRecordOrigin(crmDeps, {
1887
2444
  recordId,
1888
- sourceId: record.sourceId,
1889
- sourceRecordId: record.sourceRecordId,
2445
+ sourceId: record2.sourceId,
2446
+ sourceRecordId: record2.sourceRecordId,
1890
2447
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
1891
- payloadVersion: record.version
2448
+ payloadVersion: record2.version
1892
2449
  });
1893
2450
  return { recordId };
1894
2451
  }
@@ -2104,9 +2661,9 @@ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl
2104
2661
  const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
2105
2662
  if (!found.ok) return false;
2106
2663
  const users = await found.json().catch(() => null);
2107
- const id2 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
2108
- if (!id2) return false;
2109
- return updateClerkUserMetadata(secretKey, id2, input, fetchImpl);
2664
+ const id4 = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
2665
+ if (!id4) return false;
2666
+ return updateClerkUserMetadata(secretKey, id4, input, fetchImpl);
2110
2667
  }
2111
2668
  async function createClerkUser(secretKey, input, fetchImpl = fetch) {
2112
2669
  const { path, body } = clerkUserRequest(input);
@@ -2162,8 +2719,8 @@ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
2162
2719
  const user = Array.isArray(data) ? data[0] : void 0;
2163
2720
  return user ? toRecord(user) : null;
2164
2721
  }
2165
- async function clerkGetUser(secretKey, id2, fetchImpl = fetch) {
2166
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id2)}`, secretKey, fetchImpl).catch(() => null);
2722
+ async function clerkGetUser(secretKey, id4, fetchImpl = fetch) {
2723
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id4)}`, secretKey, fetchImpl).catch(() => null);
2167
2724
  return data ? toRecord(data) : null;
2168
2725
  }
2169
2726
  async function clerkListUsers(secretKey, fetchImpl = fetch) {
@@ -2172,15 +2729,15 @@ async function clerkListUsers(secretKey, fetchImpl = fetch) {
2172
2729
  const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
2173
2730
  const page = Array.isArray(data) ? data : [];
2174
2731
  for (const u of page) {
2175
- const record = toRecord(u);
2176
- if (record) out.push(record);
2732
+ const record2 = toRecord(u);
2733
+ if (record2) out.push(record2);
2177
2734
  }
2178
2735
  if (page.length < PAGE) break;
2179
2736
  }
2180
2737
  return out;
2181
2738
  }
2182
- async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
2183
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id2)}/metadata`, {
2739
+ async function clerkSetRole(secretKey, id4, role, fetchImpl = fetch) {
2740
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id4)}/metadata`, {
2184
2741
  method: "PATCH",
2185
2742
  headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
2186
2743
  body: JSON.stringify({ public_metadata: { role } })
@@ -2556,15 +3113,15 @@ function rowToTier(row, groupId) {
2556
3113
  }
2557
3114
  async function loadTiers(db, group) {
2558
3115
  const groupId = String(group.id ?? "");
2559
- let rows;
3116
+ let rows2;
2560
3117
  try {
2561
- rows = (await db.query({
3118
+ rows2 = (await db.query({
2562
3119
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
2563
3120
  })).tiers;
2564
3121
  } catch {
2565
- rows = void 0;
3122
+ rows2 = void 0;
2566
3123
  }
2567
- const list = Array.isArray(rows) ? rows.map((row) => rowToTier(row, groupId)) : [];
3124
+ const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
2568
3125
  return resolveTiers(group, list);
2569
3126
  }
2570
3127
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -2575,32 +3132,6 @@ async function loadOfferableTiers(db, group, hasSecretKey) {
2575
3132
  var SIGNUP_CONTROL_SCHEMA_VERSION = 1;
2576
3133
  var SIGNUP_CONTROL_PATH = "/api/signup-control";
2577
3134
 
2578
- // src/signup-control-canonical.ts
2579
- var encoder = new TextEncoder();
2580
- function canonicalValue(value) {
2581
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2582
- return JSON.stringify(value);
2583
- }
2584
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
2585
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
2586
- const record = value;
2587
- const entries = Object.keys(record).sort().flatMap((key) => {
2588
- const item = record[key];
2589
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
2590
- });
2591
- return `{${entries.join(",")}}`;
2592
- }
2593
- function canonicalSignupControl(value) {
2594
- return canonicalValue(value);
2595
- }
2596
- async function signupControlDigest(document) {
2597
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
2598
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2599
- }
2600
- async function createSignupControlEnvelope(document) {
2601
- return { document, digest: await signupControlDigest(document) };
2602
- }
2603
-
2604
3135
  // src/membership-page.ts
2605
3136
  var MEMBERSHIP_PAGE_RENDERER = "membership-cards-v1";
2606
3137
  var TIER_ID = /^[a-z][a-z0-9-]{0,79}$/;
@@ -2641,10 +3172,10 @@ function boundedString(value, path, errors, max, required = false) {
2641
3172
  errors.push(`${path} must be a string`);
2642
3173
  return void 0;
2643
3174
  }
2644
- const text2 = value.trim();
2645
- if (required && !text2) errors.push(`${path} is required`);
2646
- if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2647
- return text2;
3175
+ const text3 = value.trim();
3176
+ if (required && !text3) errors.push(`${path} is required`);
3177
+ if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
3178
+ return text3;
2648
3179
  }
2649
3180
  function validateMembershipPageDocument(input, options = {}) {
2650
3181
  const errors = [];
@@ -2693,8 +3224,8 @@ function validateMembershipPageDocument(input, options = {}) {
2693
3224
  else {
2694
3225
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
2695
3226
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
2696
- const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2697
- return text2 ? [text2] : [];
3227
+ const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
3228
+ return text3 ? [text3] : [];
2698
3229
  });
2699
3230
  }
2700
3231
  }
@@ -2773,7 +3304,7 @@ function membershipPageDocument(input, options = {}) {
2773
3304
  var SLUG4 = /^[a-z0-9][a-z0-9-]{1,62}$/;
2774
3305
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
2775
3306
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
2776
- var PRICE = /^price_[A-Za-z0-9]{12,}$/;
3307
+ var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
2777
3308
  var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2778
3309
  "schemaVersion",
2779
3310
  "revision",
@@ -2785,14 +3316,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
2785
3316
  "application",
2786
3317
  "policy",
2787
3318
  "scheduling",
2788
- "theme"
3319
+ "theme",
3320
+ "giftOffers"
2789
3321
  ]);
2790
3322
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
2791
3323
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2792
3324
  function unknownKeys(value, allowed, path, errors) {
2793
3325
  for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
2794
3326
  }
2795
- function text(value, path, errors, max = 65536) {
3327
+ function text2(value, path, errors, max = 65536) {
2796
3328
  if (typeof value !== "string" || !value.trim()) {
2797
3329
  errors.push(`${path} is required`);
2798
3330
  return "";
@@ -2815,25 +3347,25 @@ function tiersOf(value, errors) {
2815
3347
  return [];
2816
3348
  }
2817
3349
  unknownKeys(item, TIER_KEYS2, path, errors);
2818
- const id2 = text(item.id, `${path}.id`, errors, 80);
2819
- if (id2 && !TIER.test(id2)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2820
- if (seen.has(id2)) errors.push(`${path}.id duplicates "${id2}"`);
2821
- seen.add(id2);
2822
- const name = text(item.name, `${path}.name`, errors, 160);
3350
+ const id4 = text2(item.id, `${path}.id`, errors, 80);
3351
+ if (id4 && !TIER.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
3352
+ if (seen.has(id4)) errors.push(`${path}.id duplicates "${id4}"`);
3353
+ seen.add(id4);
3354
+ const name = text2(item.name, `${path}.name`, errors, 160);
2823
3355
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
2824
3356
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
2825
3357
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
2826
3358
  errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
2827
3359
  }
2828
- if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
3360
+ if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
2829
3361
  errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
2830
3362
  }
2831
3363
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
2832
3364
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
2833
3365
  }
2834
- const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
3366
+ const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
2835
3367
  return [{
2836
- id: id2,
3368
+ id: id4,
2837
3369
  name,
2838
3370
  available: item.available === true,
2839
3371
  sortOrder: Number(item.sortOrder) || 0,
@@ -2880,7 +3412,7 @@ function policyOf(value, errors) {
2880
3412
  const result = {};
2881
3413
  for (const key of allowed) {
2882
3414
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
2883
- result[key] = text(input[key], `document.policy.${key}`, errors);
3415
+ result[key] = text2(input[key], `document.policy.${key}`, errors);
2884
3416
  }
2885
3417
  return result;
2886
3418
  }
@@ -2894,12 +3426,18 @@ function themeOf(value, errors) {
2894
3426
  }
2895
3427
  unknownKeys(input, /* @__PURE__ */ new Set(["tokens"]), "document.theme", errors);
2896
3428
  if (Object.keys(tokens).length > 256) errors.push("document.theme.tokens supports at most 256 entries");
3429
+ let serializedLength = 0;
2897
3430
  for (const [key, token] of Object.entries(tokens)) {
2898
- if (!/^--[a-z0-9-]{1,100}$/.test(key)) errors.push(`document.theme.tokens.${key} must be a CSS custom-property name`);
2899
- if (typeof token !== "string" && typeof token !== "number" || String(token).length > 1e3) {
2900
- errors.push(`document.theme.tokens.${key} must be a bounded string or number`);
3431
+ if (!/^--ui-[a-z0-9][a-z0-9-]{0,94}$/.test(key)) {
3432
+ errors.push(`document.theme.tokens.${key} must be an allowlisted --ui-* custom property`);
3433
+ }
3434
+ const rendered = String(token).trim();
3435
+ serializedLength += key.length + rendered.length + 4;
3436
+ 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)) {
3437
+ errors.push(`document.theme.tokens.${key} must be a bounded breakout-safe CSS value`);
2901
3438
  }
2902
3439
  }
3440
+ if (serializedLength > 65536) errors.push("document.theme.tokens exceeds the serialized size bound");
2903
3441
  return { tokens };
2904
3442
  }
2905
3443
  function validateSignupControlDocument(input) {
@@ -2912,9 +3450,9 @@ function validateSignupControlDocument(input) {
2912
3450
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
2913
3451
  const source = recordOf(root.source);
2914
3452
  const target = recordOf(root.target);
2915
- const appId = text(source?.appId, "document.source.appId", errors, 63);
2916
- const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
2917
- const environment = text(target?.environment, "document.target.environment", errors, 63);
3453
+ const appId = text2(source?.appId, "document.source.appId", errors, 63);
3454
+ const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
3455
+ const environment = text2(target?.environment, "document.target.environment", errors, 63);
2918
3456
  if (appId && !SLUG4.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
2919
3457
  if (chapterId && !SLUG4.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
2920
3458
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -2935,6 +3473,8 @@ function validateSignupControlDocument(input) {
2935
3473
  else scheduling = root.scheduling;
2936
3474
  }
2937
3475
  const theme = themeOf(root.theme, errors);
3476
+ const giftOffers = root.giftOffers === void 0 ? void 0 : validateGiftOfferCatalog(root.giftOffers, new Set(tiers2.map((tier) => tier.id)));
3477
+ if (giftOffers && !giftOffers.ok) errors.push(...giftOffers.errors);
2938
3478
  if (errors.length || !page.ok) return { ok: false, errors };
2939
3479
  return { ok: true, value: {
2940
3480
  schemaVersion: 1,
@@ -2947,10 +3487,121 @@ function validateSignupControlDocument(input) {
2947
3487
  ...application ? { application } : {},
2948
3488
  ...policy ? { policy } : {},
2949
3489
  ...scheduling ? { scheduling } : {},
2950
- ...theme ? { theme } : {}
3490
+ ...theme ? { theme } : {},
3491
+ ...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
2951
3492
  } };
2952
3493
  }
2953
3494
 
3495
+ // src/signup-control-theme.ts
3496
+ var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
3497
+ var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
3498
+ var TOKEN_NAME = /^--ui-[a-z0-9][a-z0-9-]{0,94}$/;
3499
+ var DIGEST = /^sha256:[0-9a-f]{64}$/;
3500
+ var UNSAFE_VALUE = /[\u0000-\u001f\u007f\\<>{};]|\/\*|\*\/|(?:^|[^a-z])(?:url|expression)\s*\(|@import/i;
3501
+ var MAX_TOKENS = 256;
3502
+ var MAX_VALUE_LENGTH = 1e3;
3503
+ var MAX_SERIALIZED_LENGTH = 65536;
3504
+ function validatedTokens(input) {
3505
+ const entries = Object.entries(input ?? {});
3506
+ if (entries.length > MAX_TOKENS) {
3507
+ throw new TypeError(`signup-control theme supports at most ${MAX_TOKENS} tokens`);
3508
+ }
3509
+ const tokens = {};
3510
+ let serializedLength = 0;
3511
+ for (const [name, raw] of entries.sort(([left], [right]) => left.localeCompare(right))) {
3512
+ if (!TOKEN_NAME.test(name)) {
3513
+ throw new TypeError(`signup-control theme token "${name}" must be an allowlisted --ui-* property`);
3514
+ }
3515
+ if (typeof raw !== "string" && typeof raw !== "number" || typeof raw === "number" && !Number.isFinite(raw)) {
3516
+ throw new TypeError(`signup-control theme token "${name}" must be a finite string or number`);
3517
+ }
3518
+ const value = String(raw).trim();
3519
+ if (!value || value.length > MAX_VALUE_LENGTH || UNSAFE_VALUE.test(value)) {
3520
+ throw new TypeError(`signup-control theme token "${name}" has an unsafe or oversized value`);
3521
+ }
3522
+ serializedLength += name.length + value.length + 4;
3523
+ if (serializedLength > MAX_SERIALIZED_LENGTH) {
3524
+ throw new TypeError("signup-control theme exceeds the serialized size bound");
3525
+ }
3526
+ tokens[name] = value;
3527
+ }
3528
+ return tokens;
3529
+ }
3530
+ function cssFor(tokens) {
3531
+ const declarations = Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`);
3532
+ return declarations.length ? `:root {
3533
+ ${declarations.join("\n")}
3534
+ }` : "";
3535
+ }
3536
+ function stateFor(revision, options) {
3537
+ if (!revision) {
3538
+ return {
3539
+ css: cssFor(validatedTokens(options.bootstrapTokens)),
3540
+ metadata: { source: "bootstrap", revision: null, digest: null, cacheKey: "signup-control:bootstrap" }
3541
+ };
3542
+ }
3543
+ if (!Number.isSafeInteger(revision.revision) || revision.revision < 1 || !DIGEST.test(revision.digest)) {
3544
+ throw new TypeError("signup-control theme revision metadata is invalid");
3545
+ }
3546
+ return {
3547
+ css: cssFor(validatedTokens(revision.theme?.tokens)),
3548
+ metadata: {
3549
+ source: "active",
3550
+ revision: revision.revision,
3551
+ digest: revision.digest,
3552
+ cacheKey: `signup-control:${revision.digest}`
3553
+ }
3554
+ };
3555
+ }
3556
+ function attributes(metadata) {
3557
+ return [
3558
+ `id="${SIGNUP_CONTROL_THEME_STYLE_ID}"`,
3559
+ `data-signup-control-source="${metadata.source}"`,
3560
+ `data-signup-control-cache-key="${metadata.cacheKey}"`,
3561
+ ...metadata.revision === null ? [] : [`data-signup-control-revision="${metadata.revision}"`],
3562
+ ...metadata.digest === null ? [] : [`data-signup-control-digest="${metadata.digest}"`]
3563
+ ].join(" ");
3564
+ }
3565
+ function renderSignupControlThemeHead(revision, options = {}) {
3566
+ const { css, metadata } = stateFor(revision, options);
3567
+ return { css, metadata, head: `<style ${attributes(metadata)}>${css}</style>` };
3568
+ }
3569
+ function injectSignupControlThemeHead(html, rendered, marker = SIGNUP_CONTROL_THEME_MARKER) {
3570
+ const existing = /<style\b(?=[^>]*\bid\s*=\s*["']odla-signup-control-theme["'])[^>]*>[\s\S]*?<\/style>/gi;
3571
+ const existingBlocks = html.match(existing) ?? [];
3572
+ if (existingBlocks.length > 1) {
3573
+ throw new TypeError("signup-control theme style block must appear at most once");
3574
+ }
3575
+ if (existingBlocks.length === 1) return html.replace(existing, rendered.head);
3576
+ const markerIndex = html.indexOf(marker);
3577
+ if (markerIndex >= 0) {
3578
+ if (html.indexOf(marker, markerIndex + marker.length) >= 0) {
3579
+ throw new TypeError("signup-control theme marker must appear exactly once");
3580
+ }
3581
+ return `${html.slice(0, markerIndex)}${rendered.head}${html.slice(markerIndex + marker.length)}`;
3582
+ }
3583
+ const closingHead = html.search(/<\/head\s*>/i);
3584
+ if (closingHead < 0) throw new TypeError("signup-control theme requires an HTML head");
3585
+ return `${html.slice(0, closingHead)}${rendered.head}${html.slice(closingHead)}`;
3586
+ }
3587
+ function hydrateSignupControlTheme(target, revision, options = {}) {
3588
+ const next = stateFor(revision, options);
3589
+ const currentSource = target.getAttribute("data-signup-control-source");
3590
+ const currentRevision = target.getAttribute("data-signup-control-revision");
3591
+ const currentDigest = target.getAttribute("data-signup-control-digest");
3592
+ if (currentSource === next.metadata.source && currentRevision === (next.metadata.revision === null ? null : String(next.metadata.revision)) && currentDigest === next.metadata.digest) {
3593
+ return false;
3594
+ }
3595
+ target.textContent = next.css;
3596
+ target.setAttribute("data-signup-control-source", next.metadata.source);
3597
+ target.setAttribute("data-signup-control-cache-key", next.metadata.cacheKey);
3598
+ if (next.metadata.revision === null) target.removeAttribute("data-signup-control-revision");
3599
+ else target.setAttribute("data-signup-control-revision", String(next.metadata.revision));
3600
+ if (next.metadata.digest === null) target.removeAttribute("data-signup-control-digest");
3601
+ else target.setAttribute("data-signup-control-digest", next.metadata.digest);
3602
+ return true;
3603
+ }
3604
+
2954
3605
  // src/membership-markdown.ts
2955
3606
  var MAX_BENEFITS2 = 24;
2956
3607
  var MAX_MARKDOWN2 = 65536;
@@ -2978,9 +3629,9 @@ function markdownBenefits(lines, path, errors) {
2978
3629
  errors.push(`${path} line ${index + 1} must be a Markdown bullet`);
2979
3630
  continue;
2980
3631
  }
2981
- const text2 = (match[1] ?? "").trim();
2982
- if (text2.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
2983
- if (text2) benefits.push(text2);
3632
+ const text3 = (match[1] ?? "").trim();
3633
+ if (text3.length > 2e3) errors.push(`${path} line ${index + 1} must be at most 2,000 characters`);
3634
+ if (text3) benefits.push(text3);
2984
3635
  }
2985
3636
  if (benefits.length > MAX_BENEFITS2) errors.push(`${path} supports at most ${MAX_BENEFITS2} entries`);
2986
3637
  return benefits.slice(0, MAX_BENEFITS2);
@@ -3082,7 +3733,7 @@ var exclusions = (chapterId, paidTierIds) => [
3082
3733
  id: chapterId,
3083
3734
  reason: "provider_identity",
3084
3735
  paths: [
3085
- ...paidTierIds.map((id2) => `tiers[${id2}].stripePriceId`),
3736
+ ...paidTierIds.map((id4) => `tiers[${id4}].stripePriceId`),
3086
3737
  "groups.stripePublishableKey",
3087
3738
  "applications.stripeCustomerId",
3088
3739
  "applications.stripeSubscriptionId",
@@ -3124,9 +3775,9 @@ async function exportChapterPromotion(input) {
3124
3775
  sourceIds: [`signup-control:${document.revision}`, document.source.appId, document.target.chapterId].sort(),
3125
3776
  receiptDigests: [input.digest]
3126
3777
  };
3127
- const artifact = (kind, id2, content) => ({
3778
+ const artifact = (kind, id4, content) => ({
3128
3779
  kind,
3129
- id: id2,
3780
+ id: id4,
3130
3781
  sourceRevision: revision,
3131
3782
  provenance: promotionProvenance,
3132
3783
  content: canonicalPromotionValue(content)
@@ -3164,13 +3815,19 @@ async function exportChapterPromotion(input) {
3164
3815
  };
3165
3816
  }
3166
3817
  export {
3818
+ ADMISSION_GRANTS_PATH,
3819
+ ADMISSION_GRANTS_SCHEMA_VERSION,
3167
3820
  CHAPTER_PROMOTION_ADAPTER,
3168
3821
  DEFAULT_CHAPTER_COPY,
3169
3822
  DEFAULT_SHARE_FIELDS,
3823
+ GIFT_OFFER_CATALOG_VERSION,
3824
+ MAX_GIFT_OFFERS,
3170
3825
  MEMBERSHIP_PAGE_RENDERER,
3171
3826
  SCHEDULING_DEFAULTS,
3172
3827
  SIGNUP_CONTROL_PATH,
3173
3828
  SIGNUP_CONTROL_SCHEMA_VERSION,
3829
+ SIGNUP_CONTROL_THEME_MARKER,
3830
+ SIGNUP_CONTROL_THEME_STYLE_ID,
3174
3831
  applicantProfile,
3175
3832
  applicationBookingUpdate,
3176
3833
  applicationSummary,
@@ -3197,6 +3854,7 @@ export {
3197
3854
  clerkListUsers,
3198
3855
  clerkSetRole,
3199
3856
  clerkUserRequest,
3857
+ createAdmissionGrantEnvelope,
3200
3858
  createChapterIntegration,
3201
3859
  createClerkInvitation,
3202
3860
  createClerkUser,
@@ -3207,13 +3865,18 @@ export {
3207
3865
  emailGroupFrom,
3208
3866
  endForSlot,
3209
3867
  exportChapterPromotion,
3868
+ findAdmissionGrant,
3210
3869
  findApplicationRef,
3870
+ findGiftOffer,
3211
3871
  findTier,
3212
3872
  firstPaymentPatch,
3213
3873
  formatChapterCopy,
3214
3874
  formationFields,
3215
3875
  getVaultSecret,
3876
+ giftOfferActive,
3216
3877
  hasDisclaimerAck,
3878
+ hydrateSignupControlTheme,
3879
+ injectSignupControlThemeHead,
3217
3880
  introIdempotencyKey,
3218
3881
  isAdminRole,
3219
3882
  isAlreadySent,
@@ -3223,8 +3886,10 @@ export {
3223
3886
  joinConfig,
3224
3887
  joinConfigTiers,
3225
3888
  leaderCrmConfig,
3889
+ loadLatestAdmissionRevision,
3226
3890
  loadOfferableTiers,
3227
3891
  loadTiers,
3892
+ materializeAdmissionGrants,
3228
3893
  meetingCreateRow,
3229
3894
  meetingRescheduleUpdate,
3230
3895
  memberApplication,
@@ -3232,6 +3897,7 @@ export {
3232
3897
  membershipPageDocument,
3233
3898
  membershipTierPresentationFromMarkdown,
3234
3899
  networkSourceTag,
3900
+ normalizeAdmissionEmail,
3235
3901
  normalizeSharedRecord,
3236
3902
  normalizeWebhookEvent,
3237
3903
  offerableTiers,
@@ -3240,15 +3906,18 @@ export {
3240
3906
  planDelivery,
3241
3907
  projectApplicant,
3242
3908
  projectSharedRecord,
3909
+ publicGiftOffers,
3243
3910
  reconcileMeetings,
3244
3911
  refundedPatch,
3245
3912
  render,
3913
+ renderSignupControlThemeHead,
3246
3914
  renderSummary,
3247
3915
  renderTemplateBody,
3248
3916
  renewalPatch,
3249
3917
  resolveApplication,
3250
3918
  resolveAuth,
3251
3919
  resolveChapterCopy,
3920
+ resolveJoinContinuation,
3252
3921
  resolveLeaderFormation,
3253
3922
  resolvePipeline,
3254
3923
  resolveScheduling,
@@ -3271,6 +3940,8 @@ export {
3271
3940
  tierPayable,
3272
3941
  updateClerkUserMetadata,
3273
3942
  updateClerkUserMetadataByEmail,
3943
+ validateAdmissionGrantDocument,
3944
+ validateGiftOfferCatalog,
3274
3945
  validateMembershipPageDocument,
3275
3946
  validateScheduling,
3276
3947
  validateSignupControlDocument,