@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.
@@ -22,6 +22,7 @@ var worker_exports = {};
22
22
  __export(worker_exports, {
23
23
  chapterWorker: () => chapterWorker,
24
24
  createWorkerContext: () => createWorkerContext,
25
+ deliverAdmissionGrants: () => deliverAdmissionGrants,
25
26
  deliverSignupControl: () => deliverSignupControl,
26
27
  renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
27
28
  withActiveSignupControlTheme: () => withActiveSignupControlTheme
@@ -139,7 +140,7 @@ function createWorkerContext(options) {
139
140
  }
140
141
 
141
142
  // src/worker-routes.ts
142
- var import_crm3 = require("@odla-ai/crm");
143
+ var import_crm4 = require("@odla-ai/crm");
143
144
 
144
145
  // src/discussion-reference-security.ts
145
146
  async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
@@ -197,6 +198,205 @@ async function applicationIdForSubmission(submissionId) {
197
198
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
198
199
  }
199
200
 
201
+ // src/admission-grants-contract.ts
202
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
203
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
204
+
205
+ // src/signup-control-canonical.ts
206
+ var encoder = new TextEncoder();
207
+ function canonicalValue(value) {
208
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
209
+ return JSON.stringify(value);
210
+ }
211
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
212
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
213
+ const record2 = value;
214
+ const entries = Object.keys(record2).sort().flatMap((key) => {
215
+ const item = record2[key];
216
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
217
+ });
218
+ return `{${entries.join(",")}}`;
219
+ }
220
+ function canonicalSignupControl(value) {
221
+ return canonicalValue(value);
222
+ }
223
+ async function signupControlDigest(document) {
224
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
225
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
226
+ }
227
+
228
+ // src/admission-grants.ts
229
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
230
+ var MAX_GRANTS = 2e3;
231
+ var MAX_NOTE = 2e3;
232
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
233
+ var isAdmissionEmail = (value) => EMAIL_RE.test(value);
234
+ function normalizeAdmissionEmail(email) {
235
+ return email.trim().toLowerCase();
236
+ }
237
+ function validateAdmissionGrantDocument(input) {
238
+ const errors = [];
239
+ const root = input && typeof input === "object" ? input : {};
240
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
241
+ const revision = Number(root.revision);
242
+ const generatedAt = Number(root.generatedAt);
243
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
244
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
245
+ const source = root.source && typeof root.source === "object" ? root.source : {};
246
+ const target = root.target && typeof root.target === "object" ? root.target : {};
247
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
248
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
249
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
250
+ const environment = typeof target.environment === "string" ? target.environment : "";
251
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
252
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
253
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
254
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
255
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
256
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
257
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
258
+ const grants = [];
259
+ const seenIds = /* @__PURE__ */ new Set();
260
+ const seenEmails = /* @__PURE__ */ new Set();
261
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
262
+ const grant = raw && typeof raw === "object" ? raw : {};
263
+ const id = typeof grant.id === "string" ? grant.id : "";
264
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
265
+ const normalized = normalizeAdmissionEmail(email);
266
+ const decision = grant.decision;
267
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
268
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
269
+ if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
270
+ if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
271
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
272
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
273
+ if (decision !== "waive_interview" && decision !== "require_interview") {
274
+ errors.push(`document.grants[${index}].decision is invalid`);
275
+ }
276
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
277
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
278
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
279
+ }
280
+ seenIds.add(id);
281
+ seenEmails.add(normalized);
282
+ grants.push({
283
+ id,
284
+ email,
285
+ decision,
286
+ ...note ? { note } : {},
287
+ ...expiresAt !== void 0 ? { expiresAt } : {}
288
+ });
289
+ }
290
+ if (errors.length) return { ok: false, errors };
291
+ return {
292
+ ok: true,
293
+ value: {
294
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
295
+ revision,
296
+ generatedAt,
297
+ source: { appId: sourceId, actor: sourceActor },
298
+ target: { chapterId, environment },
299
+ grants
300
+ }
301
+ };
302
+ }
303
+ var rows = (value) => Array.isArray(value) ? value : [];
304
+ function asGrant(row) {
305
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
306
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
307
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
308
+ return row;
309
+ }
310
+ async function findAdmissionGrant(db, targetId, email, now) {
311
+ const emailNormalized = normalizeAdmissionEmail(email);
312
+ if (!isAdmissionEmail(emailNormalized)) return null;
313
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
314
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
315
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
316
+ )).sort((a, b) => {
317
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
318
+ return scope || b.createdAt - a.createdAt;
319
+ });
320
+ return active[0] ?? null;
321
+ }
322
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
323
+ const where = { sourceId, targetId, environment };
324
+ if (db.aggregate) {
325
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
326
+ }
327
+ const found = await db.query({ admissionGrantRevisions: {} });
328
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
329
+ }
330
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
331
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
332
+ const result = await db.query({ admissionGrantRevisions: { $: {
333
+ where: { sourceId, targetId, environment },
334
+ order: { revision: "desc" },
335
+ limit: 1
336
+ } } });
337
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
338
+ return row ? row : null;
339
+ }
340
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
341
+ const { document, digest } = envelope;
342
+ const current = rows((await db.query({ admissionGrants: { $: {
343
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
344
+ limit: MAX_GRANTS
345
+ } } })).admissionGrants);
346
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
347
+ const ops = [];
348
+ for (const row of current) {
349
+ const id = String(row.id ?? "");
350
+ if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
351
+ active: false,
352
+ revokedAt: receivedAt,
353
+ revokedBy: document.source.actor,
354
+ updatedAt: receivedAt,
355
+ updatedBy: document.source.actor
356
+ } });
357
+ }
358
+ for (const grant of document.grants) {
359
+ const id = `admission:parent:${document.source.appId}:${grant.id}`;
360
+ const existing = current.find((row) => row.id === id);
361
+ ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
362
+ id,
363
+ sourceGrantId: grant.id,
364
+ sourceScope: "parent",
365
+ sourceId: document.source.appId,
366
+ targetId: document.target.chapterId,
367
+ email: grant.email,
368
+ emailNormalized: normalizeAdmissionEmail(grant.email),
369
+ decision: grant.decision,
370
+ ...grant.note ? { note: grant.note } : {},
371
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
372
+ active: true,
373
+ sourceRevision: document.revision,
374
+ createdAt: Number(existing?.createdAt ?? receivedAt),
375
+ createdBy: String(existing?.createdBy ?? document.source.actor),
376
+ updatedAt: receivedAt,
377
+ updatedBy: document.source.actor
378
+ } });
379
+ const retract = [
380
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
381
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
382
+ ];
383
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
384
+ }
385
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
386
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
387
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
388
+ id: revisionId,
389
+ sourceId: document.source.appId,
390
+ targetId: document.target.chapterId,
391
+ environment: document.target.environment,
392
+ revision: document.revision,
393
+ digest,
394
+ active: true,
395
+ receivedAt
396
+ } });
397
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
398
+ }
399
+
200
400
  // src/member.ts
201
401
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
202
402
  var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
@@ -248,9 +448,9 @@ function resolveApplication(a) {
248
448
  validateEmail: a?.validateEmail ?? true
249
449
  };
250
450
  }
251
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
451
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
252
452
  function isValidEmail(value) {
253
- return typeof value === "string" && EMAIL_RE.test(value);
453
+ return typeof value === "string" && EMAIL_RE2.test(value);
254
454
  }
255
455
  function clampArray(value, max) {
256
456
  if (!Array.isArray(value)) return value;
@@ -300,6 +500,15 @@ async function submitApplication(db, chapter, fields, opts) {
300
500
  }
301
501
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
302
502
  if (opts.groupId) row.groupId = opts.groupId;
503
+ if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
504
+ row.emailNormalized = normalizeAdmissionEmail(fields.email);
505
+ }
506
+ if (opts.admissionGrant?.decision === "waive_interview") {
507
+ row.admissionGrantId = opts.admissionGrant.id;
508
+ row.admissionSource = opts.admissionGrant.sourceScope;
509
+ row.interviewWaivedAt = opts.now;
510
+ }
511
+ if (opts.giftOfferId) row.giftOfferId = opts.giftOfferId;
303
512
  if (typeof fields.tierId === "string" && fields.tierId) {
304
513
  if (opts.tierIds && !opts.tierIds.includes(fields.tierId)) {
305
514
  return { ok: false, error: "tierId is not an offered tier" };
@@ -307,8 +516,17 @@ async function submitApplication(db, chapter, fields, opts) {
307
516
  row.tierId = fields.tierId;
308
517
  }
309
518
  if (acked) row.disclaimerAckAt = opts.now;
519
+ const ops = [
520
+ { t: "update", ns: "applications", id, attrs: row }
521
+ ];
522
+ if (opts.admissionGrant?.decision === "waive_interview") {
523
+ ops.push({ t: "update", ns: "admissionGrants", id: opts.admissionGrant.id, attrs: {
524
+ consumedAt: opts.now,
525
+ consumedApplicationId: id
526
+ } });
527
+ }
310
528
  const { duplicate } = await db.transact(
311
- [{ t: "update", ns: "applications", id, attrs: row }],
529
+ ops,
312
530
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
313
531
  );
314
532
  return { ok: true, id, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
@@ -342,8 +560,8 @@ function tierPayable(tier, group, hasSecretKey) {
342
560
  function byOrder(a, b) {
343
561
  return a.sortOrder - b.sortOrder || a.id.localeCompare(b.id);
344
562
  }
345
- function resolveTiers(group, rows2 = []) {
346
- const stored = rows2.filter((tier) => tier.active);
563
+ function resolveTiers(group, rows4 = []) {
564
+ const stored = rows4.filter((tier) => tier.active);
347
565
  if (stored.length) return [...stored].sort(byOrder);
348
566
  return [{
349
567
  id: "standard",
@@ -389,15 +607,15 @@ function rowToTier(row, groupId) {
389
607
  }
390
608
  async function loadTiers(db, group) {
391
609
  const groupId = String(group.id ?? "");
392
- let rows2;
610
+ let rows4;
393
611
  try {
394
- rows2 = (await db.query({
612
+ rows4 = (await db.query({
395
613
  tiers: { $: { where: { groupId }, order: { sortOrder: "asc" }, limit: 100 } }
396
614
  })).tiers;
397
615
  } catch {
398
- rows2 = void 0;
616
+ rows4 = void 0;
399
617
  }
400
- const list = Array.isArray(rows2) ? rows2.map((row) => rowToTier(row, groupId)) : [];
618
+ const list = Array.isArray(rows4) ? rows4.map((row) => rowToTier(row, groupId)) : [];
401
619
  return resolveTiers(group, list);
402
620
  }
403
621
  async function loadOfferableTiers(db, group, hasSecretKey) {
@@ -511,341 +729,252 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
511
729
  };
512
730
  }
513
731
 
514
- // src/pipeline.ts
515
- function canTransition(from, to, p) {
516
- const fi = p.stages.indexOf(from);
517
- const ti = p.stages.indexOf(to);
518
- return fi >= 0 && ti >= 0 && ti >= fi;
732
+ // src/gift-offers.ts
733
+ var GIFT_OFFER_CATALOG_VERSION = 1;
734
+ var MAX_GIFT_OFFERS = 20;
735
+ var ID2 = /^[a-z][a-z0-9-]{1,79}$/;
736
+ var PRICE = /^price_[A-Za-z0-9]{12,}$/;
737
+ var CURRENCY = /^[a-z]{3}$/;
738
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
739
+ var text = (value, path, errors, max) => {
740
+ if (typeof value !== "string" || !value.trim()) {
741
+ errors.push(`${path} is required`);
742
+ return "";
743
+ }
744
+ if (value.trim().length > max) errors.push(`${path} must be at most ${max} characters`);
745
+ return value.trim();
746
+ };
747
+ function validateGiftOfferCatalog(input, tierIds) {
748
+ const errors = [];
749
+ const root = record(input);
750
+ if (!root) return { ok: false, errors: ["document.giftOffers must be an object"] };
751
+ for (const key of Object.keys(root)) if (key !== "schemaVersion" && key !== "offers") errors.push(`document.giftOffers.${key} is not supported`);
752
+ if (root.schemaVersion !== GIFT_OFFER_CATALOG_VERSION) errors.push("document.giftOffers.schemaVersion must be 1");
753
+ if (!Array.isArray(root.offers)) errors.push("document.giftOffers.offers must be an array");
754
+ const rawOffers = Array.isArray(root.offers) ? root.offers : [];
755
+ if (rawOffers.length > MAX_GIFT_OFFERS) errors.push(`document.giftOffers.offers supports at most ${MAX_GIFT_OFFERS} entries`);
756
+ const ids = /* @__PURE__ */ new Set();
757
+ const offers = [];
758
+ for (const [index, raw] of rawOffers.slice(0, MAX_GIFT_OFFERS).entries()) {
759
+ const path = `document.giftOffers.offers[${index}]`;
760
+ const item = record(raw);
761
+ if (!item) {
762
+ errors.push(`${path} must be an object`);
763
+ continue;
764
+ }
765
+ const allowed = /* @__PURE__ */ new Set(["id", "target", "billingMode", "stripePriceId", "priceCents", "currency", "redemptionLifetimeHours", "activeFrom", "activeUntil", "title", "description", "ctaLabel"]);
766
+ for (const key of Object.keys(item)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
767
+ const id = text(item.id, `${path}.id`, errors, 80);
768
+ if (id && !ID2.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
769
+ if (ids.has(id)) errors.push(`${path}.id duplicates "${id}"`);
770
+ ids.add(id);
771
+ const targetRaw = record(item.target);
772
+ let target = { type: "entitlement", entitlementKey: "invalid" };
773
+ if (targetRaw?.type === "tier") {
774
+ const tierId = text(targetRaw.tierId, `${path}.target.tierId`, errors, 80);
775
+ if (!tierIds.has(tierId)) errors.push(`${path}.target.tierId must reference a declared tier`);
776
+ target = { type: "tier", tierId };
777
+ } else if (targetRaw?.type === "entitlement") {
778
+ const entitlementKey = text(targetRaw.entitlementKey, `${path}.target.entitlementKey`, errors, 80);
779
+ if (entitlementKey && !ID2.test(entitlementKey)) errors.push(`${path}.target.entitlementKey must be a lowercase kebab-case id`);
780
+ target = { type: "entitlement", entitlementKey };
781
+ } else errors.push(`${path}.target.type must be tier or entitlement`);
782
+ if (item.billingMode !== "one_time" && item.billingMode !== "subscription") errors.push(`${path}.billingMode must be one_time or subscription`);
783
+ const stripePriceId = text(item.stripePriceId, `${path}.stripePriceId`, errors, 128);
784
+ if (stripePriceId && !PRICE.test(stripePriceId)) errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
785
+ 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`);
786
+ const currency = typeof item.currency === "string" ? item.currency.trim().toLowerCase() : "";
787
+ if (!CURRENCY.test(currency)) errors.push(`${path}.currency must be a lowercase ISO currency code`);
788
+ const lifetime = Number(item.redemptionLifetimeHours);
789
+ if (!Number.isSafeInteger(lifetime) || lifetime < 1 || lifetime > 8760) errors.push(`${path}.redemptionLifetimeHours must be an integer from 1 to 8760`);
790
+ const activeFrom = item.activeFrom === void 0 ? void 0 : Number(item.activeFrom);
791
+ const activeUntil = item.activeUntil === void 0 ? void 0 : Number(item.activeUntil);
792
+ if (activeFrom !== void 0 && (!Number.isSafeInteger(activeFrom) || activeFrom < 1)) errors.push(`${path}.activeFrom must be epoch milliseconds`);
793
+ if (activeUntil !== void 0 && (!Number.isSafeInteger(activeUntil) || activeUntil < 1)) errors.push(`${path}.activeUntil must be epoch milliseconds`);
794
+ if (activeFrom !== void 0 && activeUntil !== void 0 && activeUntil <= activeFrom) errors.push(`${path}.activeUntil must follow activeFrom`);
795
+ offers.push({
796
+ id,
797
+ target,
798
+ billingMode: item.billingMode,
799
+ stripePriceId,
800
+ priceCents: Number(item.priceCents),
801
+ currency,
802
+ redemptionLifetimeHours: lifetime,
803
+ ...activeFrom !== void 0 ? { activeFrom } : {},
804
+ ...activeUntil !== void 0 ? { activeUntil } : {},
805
+ title: text(item.title, `${path}.title`, errors, 160),
806
+ description: text(item.description, `${path}.description`, errors, 2e3),
807
+ ctaLabel: text(item.ctaLabel, `${path}.ctaLabel`, errors, 120)
808
+ });
809
+ }
810
+ return errors.length ? { ok: false, errors } : { ok: true, value: { schemaVersion: 1, offers } };
519
811
  }
520
- function canBook(status, p) {
521
- return p.bookableFrom.includes(status);
812
+ var giftOfferActive = (offer, now) => (!offer.activeFrom || offer.activeFrom <= now) && (!offer.activeUntil || offer.activeUntil > now);
813
+ function publicGiftOffers(catalog, now) {
814
+ return (catalog?.offers ?? []).filter((offer) => giftOfferActive(offer, now)).map(({ stripePriceId: _stripe, target: _target, ...offer }) => offer);
522
815
  }
523
- function canApprove(status, p) {
524
- return p.approvableFrom.includes(status);
816
+ function findGiftOffer(catalog, offerId, now) {
817
+ return catalog?.offers.find((offer) => offer.id === offerId && giftOfferActive(offer, now)) ?? null;
525
818
  }
526
819
 
527
- // src/signup-control-store.ts
528
- function asStored(row) {
529
- if (!row || typeof row.document !== "object" || row.document === null) return null;
530
- return row;
820
+ // src/clerk-roles.ts
821
+ var CLERK_API = "https://api.clerk.com";
822
+ var DEFAULT_ROLE = "provisional";
823
+ var PAGE = 100;
824
+ function toRecord(u) {
825
+ if (typeof u.id !== "string") return null;
826
+ const pm = u.public_metadata ?? {};
827
+ const privateMetadata = u.private_metadata ?? {};
828
+ const rawProfile = privateMetadata.profile;
829
+ const profile = rawProfile && typeof rawProfile === "object" && !Array.isArray(rawProfile) ? rawProfile : {};
830
+ const applicationId = typeof privateMetadata.applicationId === "string" ? privateMetadata.applicationId : void 0;
831
+ const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
832
+ const email = u.email_addresses?.[0]?.email_address;
833
+ return {
834
+ id: u.id,
835
+ email: typeof email === "string" ? email : void 0,
836
+ role,
837
+ publicMetadata: pm,
838
+ privateMetadata,
839
+ profile,
840
+ ...applicationId ? { applicationId } : {}
841
+ };
531
842
  }
532
- async function hasSignupControl(db, targetId, environment, active) {
533
- const where = { targetId, environment, ...active ? { active: true } : {} };
534
- if (db.aggregate) {
535
- return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
843
+ async function clerkGet(path, secretKey, fetchImpl) {
844
+ const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
845
+ if (!res.ok) throw new Error(`clerk GET ${path} \u2192 ${res.status}`);
846
+ return res.json();
847
+ }
848
+ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
849
+ const data = await clerkGet(`/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, secretKey, fetchImpl).catch(() => null);
850
+ const user = Array.isArray(data) ? data[0] : void 0;
851
+ return user ? toRecord(user) : null;
852
+ }
853
+ async function clerkGetUser(secretKey, id, fetchImpl = fetch) {
854
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id)}`, secretKey, fetchImpl).catch(() => null);
855
+ return data ? toRecord(data) : null;
856
+ }
857
+ async function clerkListUsers(secretKey, fetchImpl = fetch) {
858
+ const out = [];
859
+ for (let offset = 0; ; offset += PAGE) {
860
+ const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
861
+ const page = Array.isArray(data) ? data : [];
862
+ for (const u of page) {
863
+ const record2 = toRecord(u);
864
+ if (record2) out.push(record2);
865
+ }
866
+ if (page.length < PAGE) break;
536
867
  }
537
- const rows2 = (await db.query({ signupControls: {} })).signupControls;
538
- return Array.isArray(rows2) && rows2.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
868
+ return out;
539
869
  }
540
- async function loadLatestSignupControl(db, targetId, environment) {
541
- if (!await hasSignupControl(db, targetId, environment)) return null;
542
- const rows2 = (await db.query({ signupControls: { $: {
543
- where: { targetId, environment },
544
- order: { revision: "desc" },
545
- limit: 1
546
- } } })).signupControls;
547
- const latest = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
548
- return asStored(latest);
870
+ async function clerkSetRole(secretKey, id, role, fetchImpl = fetch) {
871
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id)}/metadata`, {
872
+ method: "PATCH",
873
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
874
+ body: JSON.stringify({ public_metadata: { role } })
875
+ });
876
+ return res.ok;
549
877
  }
550
- async function loadActiveSignupControl(db, targetId, environment) {
551
- if (!await hasSignupControl(db, targetId, environment, true)) return null;
552
- const rows2 = (await db.query({ signupControls: { $: {
553
- where: { targetId, environment, active: true },
554
- order: { revision: "desc" },
555
- limit: 1
556
- } } })).signupControls;
557
- const active = Array.isArray(rows2) ? [...rows2].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
558
- return asStored(active);
878
+
879
+ // src/crm-sync.ts
880
+ var import_crm3 = require("@odla-ai/crm");
881
+
882
+ // src/network.ts
883
+ var import_crm2 = require("@odla-ai/crm");
884
+
885
+ // src/network-contract.ts
886
+ var DEFAULT_SHARE_FIELDS = {
887
+ person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
888
+ company: ["name", "domain", "industry", "location", "linkedin", "notes"]
889
+ };
890
+
891
+ // src/network.ts
892
+ function sharedPersonInput(person) {
893
+ const email = person.email.toLowerCase();
894
+ const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
895
+ const input = { name: person.name ?? fullName ?? email, email };
896
+ if (input.name === "") input.name = email;
897
+ if (person.firstName) input.firstName = person.firstName;
898
+ if (person.lastName) input.lastName = person.lastName;
899
+ if (person.phone) input.phone = person.phone;
900
+ if (person.linkedin) input.linkedin = person.linkedin;
901
+ return input;
559
902
  }
560
- function controlledApplication(base, document) {
561
- const delegated = document?.application;
562
- return delegated ? { ...base, required: delegated.required, optional: delegated.optional, conditions: { ...delegated.conditions ?? {} } } : base;
903
+ function shortHash(value) {
904
+ let a = 2166136261;
905
+ let b = 2654435769;
906
+ for (let i = 0; i < value.length; i += 1) {
907
+ const n = value.charCodeAt(i);
908
+ a = Math.imul(a ^ n, 16777619);
909
+ b = Math.imul(b ^ n, 2246822507);
910
+ }
911
+ return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
563
912
  }
564
- function validateControlledApplication(chapter, document) {
565
- if (!document.application) return [];
566
- const declared = /* @__PURE__ */ new Set([...chapter.application.required, ...chapter.application.optional]);
567
- return [...document.application.required, ...document.application.optional].filter((field) => !declared.has(field)).map((field) => `document.application field "${field}" is not declared by this chapter schema`);
913
+ function networkSourceTag(type, hubRecordId, sourceId) {
914
+ const typeKey = type.toLowerCase();
915
+ const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
916
+ const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
917
+ const raw = `${prefix}:${hubRecordId}`;
918
+ if (readable && raw.length <= 64) return raw;
919
+ return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
568
920
  }
569
- function groupAttrs(document) {
570
- const first = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
571
- const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
921
+ function normalizeSharedRecord(record2, fallbackSourceId = "legacy-source") {
922
+ if ("input" in record2 && record2.version === 2) {
923
+ return {
924
+ version: 2,
925
+ sourceId: record2.source.siteId,
926
+ sourceRecordId: record2.source.recordId,
927
+ type: record2.type,
928
+ input: record2.input
929
+ };
930
+ }
931
+ if ("input" in record2) {
932
+ return {
933
+ version: 1,
934
+ sourceId: fallbackSourceId,
935
+ sourceRecordId: record2.hubRecordId,
936
+ type: record2.type,
937
+ input: record2.input
938
+ };
939
+ }
940
+ if ("type" in record2 && record2.type === "company") {
941
+ const input = { name: record2.name };
942
+ for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
943
+ if (record2[key]) input[key] = record2[key];
944
+ }
945
+ return {
946
+ version: 1,
947
+ sourceId: fallbackSourceId,
948
+ sourceRecordId: record2.hubRecordId,
949
+ type: "company",
950
+ input
951
+ };
952
+ }
572
953
  return {
573
- ...first ? { standardPriceCents: first.priceCents } : {},
574
- foundingDiscountCents: 0,
575
- ...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
576
- ...document.policy ?? {},
577
- ...document.scheduling ? { schedulingJson: document.scheduling } : {}
954
+ version: 1,
955
+ sourceId: fallbackSourceId,
956
+ sourceRecordId: record2.hubRecordId,
957
+ type: "person",
958
+ input: sharedPersonInput(record2)
578
959
  };
579
960
  }
580
- var byPublicId = (ns, value) => ({
581
- ns,
582
- attr: "id",
583
- value
584
- });
585
- async function materializeSignupControl(db, envelope, previous, receivedAt) {
586
- const { document, digest } = envelope;
587
- const currentRows = (await db.query({ tiers: { $: {
588
- where: { groupId: document.target.chapterId },
589
- limit: 100
590
- } } })).tiers ?? [];
591
- const incoming = new Set(document.tiers.map((tier) => tier.id));
592
- const ops = [];
593
- if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
594
- for (const row of currentRows) {
595
- const id2 = String(row.id ?? "");
596
- if (id2 && !incoming.has(id2)) {
597
- ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers", id2), attrs: { active: false } });
598
- }
599
- }
600
- for (const tier of document.tiers) {
601
- const tierRef = byPublicId("tiers", tier.id);
602
- ops.push({ t: "update", ns: "tiers", id: tierRef, attrs: {
603
- id: tier.id,
604
- groupId: document.target.chapterId,
605
- name: tier.name,
606
- priceCents: tier.priceCents,
607
- ...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
608
- ...tier.blurb ? { blurb: tier.blurb } : {},
609
- sortOrder: tier.sortOrder,
610
- active: tier.available,
611
- createdAt: receivedAt
612
- } });
613
- const retract = [
614
- ...!tier.stripePriceId ? ["stripePriceId"] : [],
615
- ...!tier.blurb ? ["blurb"] : []
616
- ];
617
- if (retract.length) ops.push({ t: "retract", ns: "tiers", id: tierRef, attrs: retract });
618
- }
619
- const groupRef = byPublicId("groups", document.target.chapterId);
620
- const attrs = groupAttrs(document);
621
- ops.push({
622
- t: "update",
623
- ns: "groups",
624
- id: groupRef,
625
- attrs
626
- });
627
- if (!("stripePriceId" in attrs)) {
628
- ops.push({ t: "retract", ns: "groups", id: groupRef, attrs: ["stripePriceId"] });
629
- }
630
- const id = `signup-control:${document.target.environment}:${document.revision}`;
631
- ops.push({ t: "update", ns: "signupControls", id, attrs: {
632
- id,
633
- sourceId: document.source.appId,
634
- targetId: document.target.chapterId,
635
- environment: document.target.environment,
636
- revision: document.revision,
637
- digest,
638
- document,
639
- active: true,
640
- receivedAt
641
- } });
642
- await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest}` });
643
- }
644
-
645
- // src/session.ts
646
- function applicationSummary(app) {
647
- return {
648
- id: app.id,
649
- firstName: app.firstName ?? null,
650
- lastName: app.lastName ?? null,
651
- email: app.email ?? null,
652
- status: app.status,
653
- createdAt: app.createdAt ?? null,
654
- meetingLink: app.meetingLink ?? null,
655
- paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
656
- renewalAt: app.renewalAt ?? null,
657
- canceled: app.canceled === true
658
- };
659
- }
660
- function memberApplication(app, meeting, defaultTimezone) {
661
- const summary = applicationSummary(app);
662
- let meetingAt = app.meetingAt ?? null;
663
- let meetUrl = null;
664
- let timezone = defaultTimezone;
665
- if (meeting) {
666
- timezone = meeting.timezone ?? timezone;
667
- if (meeting.status === "scheduled") {
668
- meetingAt = meeting.startAt ?? null;
669
- meetUrl = meeting.meetUrl ?? null;
670
- } else {
671
- meetingAt = null;
672
- }
673
- }
674
- return { ...summary, meetingAt, meetUrl, timezone };
675
- }
676
-
677
- // src/clerk.ts
678
- var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
679
- function clerkInviteRequest(input) {
680
- return {
681
- path: "/v1/invitations",
682
- body: {
683
- email_address: input.email,
684
- notify: true,
685
- ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
686
- ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
687
- }
688
- };
689
- }
690
- async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
691
- const { path, body } = clerkInviteRequest(input);
692
- const res = await fetchImpl(`https://api.clerk.com${path}`, {
693
- method: "POST",
694
- headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
695
- body: JSON.stringify(body)
696
- });
697
- return res.ok ? { ok: true, status: res.status } : heal(res.status);
698
- }
699
- function clerkUserRequest(input) {
700
- return {
701
- path: "/v1/users",
702
- body: {
703
- email_address: [input.email],
704
- skip_password_requirement: true,
705
- ...input.firstName ? { first_name: input.firstName } : {},
706
- ...input.lastName ? { last_name: input.lastName } : {},
707
- ...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
708
- ...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
709
- }
710
- };
711
- }
712
- async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fetch) {
713
- const res = await fetchImpl(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}/metadata`, {
714
- method: "PATCH",
715
- headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
716
- body: JSON.stringify({
717
- ...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
718
- ...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
719
- })
720
- });
721
- return res.ok;
722
- }
723
- async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
724
- const auth = { authorization: `Bearer ${secretKey}` };
725
- const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
726
- if (!found.ok) return false;
727
- const users = await found.json().catch(() => null);
728
- const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
729
- if (!id) return false;
730
- return updateClerkUserMetadata(secretKey, id, input, fetchImpl);
731
- }
732
- async function createClerkUser(secretKey, input, fetchImpl = fetch) {
733
- const { path, body } = clerkUserRequest(input);
734
- const res = await fetchImpl(`https://api.clerk.com${path}`, {
735
- method: "POST",
736
- headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
737
- body: JSON.stringify(body)
738
- });
739
- if (res.ok) return { ok: true, status: res.status };
740
- const healed = heal(res.status);
741
- const repair = input.repairMetadata ?? {
742
- ...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
743
- ...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
744
- };
745
- if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
746
- const refreshed = await updateClerkUserMetadataByEmail(secretKey, input.email, {
747
- ...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
748
- ...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
749
- }, fetchImpl).catch(() => false);
750
- return { ...healed, refreshed };
751
- }
752
-
753
- // src/network.ts
754
- var import_crm2 = require("@odla-ai/crm");
755
-
756
- // src/network-contract.ts
757
- var DEFAULT_SHARE_FIELDS = {
758
- person: ["name", "email", "firstName", "lastName", "phone", "linkedin"],
759
- company: ["name", "domain", "industry", "location", "linkedin", "notes"]
760
- };
761
-
762
- // src/network.ts
763
- function sharedPersonInput(person) {
764
- const email = person.email.toLowerCase();
765
- const fullName = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
766
- const input = { name: person.name ?? fullName ?? email, email };
767
- if (input.name === "") input.name = email;
768
- if (person.firstName) input.firstName = person.firstName;
769
- if (person.lastName) input.lastName = person.lastName;
770
- if (person.phone) input.phone = person.phone;
771
- if (person.linkedin) input.linkedin = person.linkedin;
772
- return input;
773
- }
774
- function shortHash(value) {
775
- let a = 2166136261;
776
- let b = 2654435769;
777
- for (let i = 0; i < value.length; i += 1) {
778
- const n = value.charCodeAt(i);
779
- a = Math.imul(a ^ n, 16777619);
780
- b = Math.imul(b ^ n, 2246822507);
781
- }
782
- return `${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
783
- }
784
- function networkSourceTag(type, hubRecordId, sourceId) {
785
- const typeKey = type.toLowerCase();
786
- const readable = /^[a-z0-9_-]+$/.test(hubRecordId);
787
- const prefix = sourceId ? `network:${sourceId.toLowerCase()}:${typeKey}` : `network:${typeKey}`;
788
- const raw = `${prefix}:${hubRecordId}`;
789
- if (readable && raw.length <= 64) return raw;
790
- return `network:${typeKey.slice(0, 16)}:${shortHash(`${sourceId ?? ""}\0${type}\0${hubRecordId}`)}`;
791
- }
792
- function normalizeSharedRecord(record, fallbackSourceId = "legacy-source") {
793
- if ("input" in record && record.version === 2) {
794
- return {
795
- version: 2,
796
- sourceId: record.source.siteId,
797
- sourceRecordId: record.source.recordId,
798
- type: record.type,
799
- input: record.input
800
- };
801
- }
802
- if ("input" in record) {
803
- return {
804
- version: 1,
805
- sourceId: fallbackSourceId,
806
- sourceRecordId: record.hubRecordId,
807
- type: record.type,
808
- input: record.input
809
- };
810
- }
811
- if ("type" in record && record.type === "company") {
812
- const input = { name: record.name };
813
- for (const key of ["domain", "industry", "location", "linkedin", "notes"]) {
814
- if (record[key]) input[key] = record[key];
815
- }
816
- return {
817
- version: 1,
818
- sourceId: fallbackSourceId,
819
- sourceRecordId: record.hubRecordId,
820
- type: "company",
821
- input
822
- };
823
- }
824
- return {
825
- version: 1,
826
- sourceId: fallbackSourceId,
827
- sourceRecordId: record.hubRecordId,
828
- type: "person",
829
- input: sharedPersonInput(record)
830
- };
831
- }
832
- function sharedRecordFromCrm(crm, record, target, sourceId) {
833
- if (target.fields && !target.fields[record.type]) {
834
- throw new Error(`${target.name} does not accept "${record.type}" records`);
961
+ function sharedRecordFromCrm(crm, record2, target, sourceId) {
962
+ if (target.fields && !target.fields[record2.type]) {
963
+ throw new Error(`${target.name} does not accept "${record2.type}" records`);
835
964
  }
836
- const def = crm.type(record.type);
837
- const fields = target.fields?.[record.type] ?? DEFAULT_SHARE_FIELDS[record.type];
965
+ const def = crm.type(record2.type);
966
+ const fields = target.fields?.[record2.type] ?? DEFAULT_SHARE_FIELDS[record2.type];
838
967
  if (!fields) {
839
- throw new Error(`${target.name} requires an explicit field allowlist for "${record.type}" records`);
968
+ throw new Error(`${target.name} requires an explicit field allowlist for "${record2.type}" records`);
840
969
  }
841
970
  const nameField = def.nameField ?? "name";
842
971
  const input = {};
843
972
  for (const field of /* @__PURE__ */ new Set([nameField, ...fields])) {
844
- const value = record.fields?.[field];
973
+ const value = record2.fields?.[field];
845
974
  if (value !== void 0) input[field] = value;
846
975
  }
847
- if (input[nameField] === void 0) input[nameField] = record.name;
848
- return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record.id }, type: record.type, input } : { version: 1, type: record.type, hubRecordId: record.id, input };
976
+ if (input[nameField] === void 0) input[nameField] = record2.name;
977
+ return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record2.id }, type: record2.type, input } : { version: 1, type: record2.type, hubRecordId: record2.id, input };
849
978
  }
850
979
  async function upsertPerson(deps, opts) {
851
980
  const email = opts.email.toLowerCase();
@@ -859,11 +988,11 @@ async function upsertPerson(deps, opts) {
859
988
  const created = await (0, import_crm2.createRecord)(crmDeps3, { type: "person", input: opts.input, mutationId: opts.mutationId });
860
989
  return { recordId: created.id };
861
990
  }
862
- async function findSharedRecord(deps, record, tag) {
991
+ async function findSharedRecord(deps, record2, tag) {
863
992
  const structured = await (0, import_crm2.getRecordByOrigin)(
864
993
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
865
- record.sourceId,
866
- record.sourceRecordId
994
+ record2.sourceId,
995
+ record2.sourceRecordId
867
996
  );
868
997
  if (structured) return structured;
869
998
  const mapped = await deps.db.query({ crm_tag: { $: { where: { tag }, limit: 1 } } });
@@ -872,53 +1001,53 @@ async function findSharedRecord(deps, record, tag) {
872
1001
  const found = await deps.db.query({ crm_record: { $: { where: { id: mappedId }, limit: 1 } } });
873
1002
  if (found.crm_record?.[0]) return found.crm_record[0];
874
1003
  }
875
- const def = deps.crm.type(record.type);
1004
+ const def = deps.crm.type(record2.type);
876
1005
  const emailField = def.emailField;
877
- if (emailField && typeof record.input[emailField] === "string") {
878
- const primaryEmail = record.input[emailField].toLowerCase();
1006
+ if (emailField && typeof record2.input[emailField] === "string") {
1007
+ const primaryEmail = record2.input[emailField].toLowerCase();
879
1008
  const found = await deps.db.query({
880
- crm_record: { $: { where: { type: record.type, primaryEmail }, limit: 1 } }
1009
+ crm_record: { $: { where: { type: record2.type, primaryEmail }, limit: 1 } }
881
1010
  });
882
1011
  if (found.crm_record?.[0]) return found.crm_record[0];
883
1012
  }
884
- const domain = record.input.domain;
1013
+ const domain = record2.input.domain;
885
1014
  const domainSlot = def.fields.domain?.slot;
886
1015
  if (typeof domain === "string" && domainSlot) {
887
1016
  const found = await deps.db.query({
888
- crm_record: { $: { where: { type: record.type, [domainSlot]: domain }, limit: 1 } }
1017
+ crm_record: { $: { where: { type: record2.type, [domainSlot]: domain }, limit: 1 } }
889
1018
  });
890
1019
  if (found.crm_record?.[0]) return found.crm_record[0];
891
1020
  }
892
1021
  const nameField = def.nameField ?? "name";
893
- const name = record.input[nameField];
894
- if (record.type === "company" && typeof name === "string" && name.trim()) {
1022
+ const name = record2.input[nameField];
1023
+ if (record2.type === "company" && typeof name === "string" && name.trim()) {
895
1024
  const found = await deps.db.query({
896
- crm_record: { $: { where: { type: record.type, name: name.trim() }, limit: 1 } }
1025
+ crm_record: { $: { where: { type: record2.type, name: name.trim() }, limit: 1 } }
897
1026
  });
898
1027
  if (found.crm_record?.[0]) return found.crm_record[0];
899
1028
  }
900
1029
  return void 0;
901
1030
  }
902
1031
  async function projectSharedRecord(deps, shared, options = {}) {
903
- const record = normalizeSharedRecord(shared, options.sourceId);
904
- if (!record.type.trim() || !record.sourceId.trim() || !record.sourceRecordId.trim()) {
1032
+ const record2 = normalizeSharedRecord(shared, options.sourceId);
1033
+ if (!record2.type.trim() || !record2.sourceId.trim() || !record2.sourceRecordId.trim()) {
905
1034
  throw new Error("type, source site id, and source record id must be non-empty");
906
1035
  }
907
- if (options.sourceId && options.sourceId !== record.sourceId) {
1036
+ if (options.sourceId && options.sourceId !== record2.sourceId) {
908
1037
  throw new Error("signed sender does not match payload source");
909
1038
  }
910
- const tag = networkSourceTag(record.type, record.sourceRecordId, record.version === 2 ? record.sourceId : void 0);
1039
+ const tag = networkSourceTag(record2.type, record2.sourceRecordId, record2.version === 2 ? record2.sourceId : void 0);
911
1040
  const crmDeps3 = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
912
- const existing = await findSharedRecord(deps, record, tag);
1041
+ const existing = await findSharedRecord(deps, record2, tag);
913
1042
  let recordId;
914
1043
  if (existing && typeof existing.id === "string") {
915
- await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record.input });
1044
+ await (0, import_crm2.updateRecord)(crmDeps3, { id: existing.id, input: record2.input });
916
1045
  recordId = existing.id;
917
1046
  } else {
918
- recordId = `network_${shortHash(`${record.sourceId}\0${record.type}\0${record.sourceRecordId}`)}`;
1047
+ recordId = `network_${shortHash(`${record2.sourceId}\0${record2.type}\0${record2.sourceRecordId}`)}`;
919
1048
  await (0, import_crm2.createRecord)({ ...crmDeps3, newId: () => recordId }, {
920
- type: record.type,
921
- input: record.input,
1049
+ type: record2.type,
1050
+ input: record2.input,
922
1051
  mutationId: `share-create:${tag}`
923
1052
  });
924
1053
  }
@@ -928,10 +1057,10 @@ async function projectSharedRecord(deps, shared, options = {}) {
928
1057
  );
929
1058
  await (0, import_crm2.upsertRecordOrigin)(crmDeps3, {
930
1059
  recordId,
931
- sourceId: record.sourceId,
932
- sourceRecordId: record.sourceRecordId,
1060
+ sourceId: record2.sourceId,
1061
+ sourceRecordId: record2.sourceRecordId,
933
1062
  ...options.sourceUrl ? { sourceUrl: options.sourceUrl } : {},
934
- payloadVersion: record.version
1063
+ payloadVersion: record2.version
935
1064
  });
936
1065
  return { recordId };
937
1066
  }
@@ -954,6 +1083,92 @@ async function projectApplicant(deps, applicant) {
954
1083
  }
955
1084
  }
956
1085
 
1086
+ // src/crm-sync.ts
1087
+ var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
1088
+ var crmDeps = (db, ctx) => ({
1089
+ crm: ctx.chapter.crm,
1090
+ db,
1091
+ now: () => Date.now(),
1092
+ newId: () => crypto.randomUUID(),
1093
+ chapter: ctx.chapter
1094
+ });
1095
+ function personInputFromApp(chapter, app) {
1096
+ const input = sharedPersonInput({
1097
+ email: str(app.email),
1098
+ firstName: str(app.firstName) || void 0,
1099
+ lastName: str(app.lastName) || void 0,
1100
+ phone: str(app.phone) || void 0,
1101
+ linkedin: str(app.linkedin) || void 0,
1102
+ hubRecordId: str(app.id)
1103
+ });
1104
+ for (const f of chapter.application.crmFields) {
1105
+ if (app[f] !== void 0) input[f] = app[f];
1106
+ }
1107
+ if (app.id !== void 0) input.applicationId = str(app.id);
1108
+ return input;
1109
+ }
1110
+ function billingColumns(app) {
1111
+ const status = str(app.status);
1112
+ const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
1113
+ const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
1114
+ const cols = { billingStatus };
1115
+ if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
1116
+ if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
1117
+ if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
1118
+ return cols;
1119
+ }
1120
+ async function syncApplicationToCrm(deps, opts) {
1121
+ const emailKey = str(opts.app.email).toLowerCase();
1122
+ if (!emailKey) return null;
1123
+ const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1124
+ const input = personInputFromApp(deps.chapter, opts.app);
1125
+ const { crm_record } = await deps.db.query({
1126
+ crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
1127
+ });
1128
+ const existing = crm_record?.[0] ?? null;
1129
+ const stage = opts.stage || void 0;
1130
+ let recordId;
1131
+ if (existing && typeof existing.id === "string") {
1132
+ recordId = existing.id;
1133
+ await (0, import_crm3.updateRecord)(recordDeps, { id: recordId, input });
1134
+ } else {
1135
+ const created = await (0, import_crm3.createRecord)(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
1136
+ recordId = created.id;
1137
+ }
1138
+ if (existing && stage && existing.stage !== stage) {
1139
+ await (0, import_crm3.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
1140
+ }
1141
+ await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
1142
+ await (0, import_crm3.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1143
+ return recordId;
1144
+ }
1145
+ async function backfillCrm(deps) {
1146
+ const [appsRes, usersRes] = await Promise.all([
1147
+ deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1148
+ deps.db.query({ $users: { $: { limit: 1e3 } } })
1149
+ ]);
1150
+ const seen = /* @__PURE__ */ new Set();
1151
+ let synced = 0;
1152
+ const errors = [];
1153
+ const run = async (app, stage) => {
1154
+ const key = str(app.email).toLowerCase();
1155
+ if (!key || seen.has(key)) return;
1156
+ seen.add(key);
1157
+ try {
1158
+ await syncApplicationToCrm(deps, { app, stage });
1159
+ synced += 1;
1160
+ } catch (err) {
1161
+ errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
1162
+ }
1163
+ };
1164
+ for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1165
+ for (const u of usersRes.$users ?? []) {
1166
+ if (u.deleted === true) continue;
1167
+ await run({ email: u.email, firstName: str(u.name) });
1168
+ }
1169
+ return { synced, errors };
1170
+ }
1171
+
957
1172
  // src/email.ts
958
1173
  function render(template, vars) {
959
1174
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
@@ -984,10 +1199,10 @@ function planDelivery(input) {
984
1199
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
985
1200
  const to = redirect ? input.group.debugEmail : input.to;
986
1201
  const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
987
- const text2 = redirect ? `(dev redirect; original recipient: ${input.to})
1202
+ const text3 = redirect ? `(dev redirect; original recipient: ${input.to})
988
1203
 
989
1204
  ` + render(tpl.text, vars) : render(tpl.text, vars);
990
- return { deliver: true, transport, to, subject, text: text2, redirected: redirect };
1205
+ return { deliver: true, transport, to, subject, text: text3, redirected: redirect };
991
1206
  }
992
1207
 
993
1208
  // src/notify.ts
@@ -1023,38 +1238,371 @@ async function sendTemplated(deps, input) {
1023
1238
  error = e instanceof Error ? e.message : String(e);
1024
1239
  }
1025
1240
  }
1026
- const id = deps.newId();
1027
- const row = {
1028
- id,
1029
- groupId: input.group.id,
1030
- to: decision.to,
1031
- template: input.template,
1032
- subject: decision.subject,
1033
- body: decision.text,
1034
- transport: decision.transport,
1035
- redirected: decision.redirected,
1036
- dedupeKey: input.dedupeKey,
1037
- sentAt: deps.now(),
1038
- ...input.applicationId ? { applicationId: input.applicationId } : {},
1039
- ...messageId ? { messageId } : {},
1040
- ...error ? { error } : {}
1241
+ const id = deps.newId();
1242
+ const row = {
1243
+ id,
1244
+ groupId: input.group.id,
1245
+ to: decision.to,
1246
+ template: input.template,
1247
+ subject: decision.subject,
1248
+ body: decision.text,
1249
+ transport: decision.transport,
1250
+ redirected: decision.redirected,
1251
+ dedupeKey: input.dedupeKey,
1252
+ sentAt: deps.now(),
1253
+ ...input.applicationId ? { applicationId: input.applicationId } : {},
1254
+ ...messageId ? { messageId } : {},
1255
+ ...error ? { error } : {}
1256
+ };
1257
+ await deps.db.transact([{ t: "update", ns: "emailLog", id, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
1258
+ return error ? { sent: false, reason: error } : { sent: true };
1259
+ }
1260
+ function emailGroupFrom(row) {
1261
+ const str3 = (v) => typeof v === "string" ? v : void 0;
1262
+ const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
1263
+ return {
1264
+ id: String(row.id),
1265
+ name: String(row.name ?? ""),
1266
+ replyTo: str3(row.replyTo) ?? "",
1267
+ debugEmail: str3(row.debugEmail),
1268
+ refundPolicyText: str3(row.refundPolicyText),
1269
+ commitmentText: str3(row.commitmentText),
1270
+ normsText: str3(row.normsText),
1271
+ emailTemplates: templates
1272
+ };
1273
+ }
1274
+
1275
+ // src/pipeline.ts
1276
+ function canTransition(from, to, p) {
1277
+ const fi = p.stages.indexOf(from);
1278
+ const ti = p.stages.indexOf(to);
1279
+ return fi >= 0 && ti >= 0 && ti >= fi;
1280
+ }
1281
+ function canBook(status, p) {
1282
+ return p.bookableFrom.includes(status);
1283
+ }
1284
+ function canApprove(status, p) {
1285
+ return p.approvableFrom.includes(status);
1286
+ }
1287
+
1288
+ // src/approve-application.ts
1289
+ async function approveApplication(input) {
1290
+ const { db, env, ctx, app } = input;
1291
+ const id = String(app.id ?? "");
1292
+ const status = String(app.status ?? "");
1293
+ if (!id) return { ok: false, status: 404, error: "not found" };
1294
+ if (input.reason === "manual" && status !== "approved" && !canApprove(status, ctx.chapter.pipeline)) {
1295
+ return { ok: false, status: 409, error: `cannot approve from status "${status}"` };
1296
+ }
1297
+ if (input.reason === "interview_waiver") {
1298
+ if (typeof app.interviewWaivedAt !== "number") return { ok: false, status: 409, error: "interview waiver required" };
1299
+ if (status !== "approved" && !canTransition(status, "approved", ctx.chapter.pipeline)) {
1300
+ return { ok: false, status: 409, error: `cannot approve from status "${status}"` };
1301
+ }
1302
+ }
1303
+ const write = await db.transact(
1304
+ [{ t: "update", ns: "applications", id, attrs: { status: "approved" } }],
1305
+ { mutationId: `approve:${id}` }
1306
+ );
1307
+ const approved = { ...app, status: "approved" };
1308
+ await syncApplicationToCrm(crmDeps(db, ctx), { app: approved, stage: "approved" }).catch(() => void 0);
1309
+ const { promoteTo, send } = ctx.chapter.operations.onApprove;
1310
+ let rolePromoted = false;
1311
+ const secret = await getVaultSecret(db, "clerk_secret_key");
1312
+ if (promoteTo !== false && secret) {
1313
+ let userId = typeof app.clerkUserId === "string" ? app.clerkUserId : null;
1314
+ if (!userId && typeof app.email === "string") {
1315
+ const found = await clerkGetUserByEmail(secret, app.email);
1316
+ userId = found?.id ?? null;
1317
+ if (userId) await db.transact([{ t: "update", ns: "applications", id, attrs: { clerkUserId: userId } }]);
1318
+ }
1319
+ if (userId) rolePromoted = await clerkSetRole(secret, userId, promoteTo);
1320
+ }
1321
+ let emailLogged = false;
1322
+ const groups = (await db.query({ groups: { $: { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 } } })).groups;
1323
+ const group = Array.isArray(groups) ? groups[0] : void 0;
1324
+ if (send !== false && group && typeof app.email === "string") {
1325
+ const result = await sendTemplated(
1326
+ { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
1327
+ {
1328
+ group: emailGroupFrom(group),
1329
+ template: send,
1330
+ to: app.email,
1331
+ vars: { firstName: String(app.firstName ?? ""), membersUrl: `${input.origin}/members/` },
1332
+ applicationId: id,
1333
+ dedupeKey: `approve:${id}`
1334
+ }
1335
+ );
1336
+ emailLogged = result.sent;
1337
+ }
1338
+ return { ok: true, status: 200, rolePromoted, emailLogged, duplicate: write.duplicate };
1339
+ }
1340
+
1341
+ // src/join-continuation.ts
1342
+ function resolveJoinContinuation(input) {
1343
+ if (input.status === "approved") return "accepted";
1344
+ if (input.interviewWaived) {
1345
+ if (!input.payable) return "approve";
1346
+ if (input.status === input.initialStatus) {
1347
+ return input.subscriptionStarted ? "paymentPending" : "payment";
1348
+ }
1349
+ return "approve";
1350
+ }
1351
+ if (!input.payable) return "booking";
1352
+ if (input.status === input.initialStatus) {
1353
+ return input.subscriptionStarted ? "paymentPending" : "payment";
1354
+ }
1355
+ if (canBook(input.status, input.pipeline)) return "booking";
1356
+ return null;
1357
+ }
1358
+
1359
+ // src/join-continuation-server.ts
1360
+ async function continueJoin(db, ctx, env, origin, app, group) {
1361
+ if (typeof app.giftOfferId === "string") {
1362
+ if (typeof app.giftClaimId === "string") return "gift";
1363
+ return typeof app.giftCheckoutStartedAt === "number" ? "paymentPending" : "payment";
1364
+ }
1365
+ const stripeKey = await getVaultSecret(db, "stripe_secret_key");
1366
+ const offered = await loadOfferableTiers(db, group, Boolean(stripeKey));
1367
+ const tier = findTier(offered, typeof app.tierId === "string" ? app.tierId : null);
1368
+ const payable = Boolean(tier) && !tierIsFree(tier) && tierPayable(tier, group, Boolean(stripeKey));
1369
+ const state = resolveJoinContinuation({
1370
+ status: String(app.status ?? ""),
1371
+ initialStatus: ctx.chapter.pipeline.initial,
1372
+ pipeline: ctx.chapter.pipeline,
1373
+ payable,
1374
+ subscriptionStarted: typeof app.stripeSubscriptionId === "string" && app.stripeSubscriptionId !== "",
1375
+ interviewWaived: typeof app.interviewWaivedAt === "number"
1376
+ });
1377
+ if (state !== "approve") return state;
1378
+ const approved = await approveApplication({ db, env, ctx, app, origin, reason: "interview_waiver" });
1379
+ return approved.ok ? "accepted" : null;
1380
+ }
1381
+
1382
+ // src/signup-control-store.ts
1383
+ function asStored(row) {
1384
+ if (!row || typeof row.document !== "object" || row.document === null) return null;
1385
+ return row;
1386
+ }
1387
+ async function hasSignupControl(db, targetId, environment, active) {
1388
+ const where = { targetId, environment, ...active ? { active: true } : {} };
1389
+ if (db.aggregate) {
1390
+ return Number((await db.aggregate("signupControls", { count: true }, { where })).count ?? 0) > 0;
1391
+ }
1392
+ const rows4 = (await db.query({ signupControls: {} })).signupControls;
1393
+ return Array.isArray(rows4) && rows4.some((row) => row.targetId === targetId && row.environment === environment && (!active || row.active === true));
1394
+ }
1395
+ async function loadLatestSignupControl(db, targetId, environment) {
1396
+ if (!await hasSignupControl(db, targetId, environment)) return null;
1397
+ const rows4 = (await db.query({ signupControls: { $: {
1398
+ where: { targetId, environment },
1399
+ order: { revision: "desc" },
1400
+ limit: 1
1401
+ } } })).signupControls;
1402
+ const latest = Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
1403
+ return asStored(latest);
1404
+ }
1405
+ async function loadActiveSignupControl(db, targetId, environment) {
1406
+ if (!await hasSignupControl(db, targetId, environment, true)) return null;
1407
+ const rows4 = (await db.query({ signupControls: { $: {
1408
+ where: { targetId, environment, active: true },
1409
+ order: { revision: "desc" },
1410
+ limit: 1
1411
+ } } })).signupControls;
1412
+ const active = Array.isArray(rows4) ? [...rows4].sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0] : void 0;
1413
+ return asStored(active);
1414
+ }
1415
+ function controlledApplication(base, document) {
1416
+ const delegated = document?.application;
1417
+ return delegated ? { ...base, required: delegated.required, optional: delegated.optional, conditions: { ...delegated.conditions ?? {} } } : base;
1418
+ }
1419
+ function validateControlledApplication(chapter, document) {
1420
+ if (!document.application) return [];
1421
+ const declared = /* @__PURE__ */ new Set([...chapter.application.required, ...chapter.application.optional]);
1422
+ return [...document.application.required, ...document.application.optional].filter((field) => !declared.has(field)).map((field) => `document.application field "${field}" is not declared by this chapter schema`);
1423
+ }
1424
+ function groupAttrs(document) {
1425
+ const first4 = [...document.tiers].filter((tier) => tier.available).sort((a, b) => a.sortOrder - b.sortOrder)[0];
1426
+ const firstPaid = [...document.tiers].find((tier) => tier.available && tier.priceCents > 0);
1427
+ return {
1428
+ ...first4 ? { standardPriceCents: first4.priceCents } : {},
1429
+ foundingDiscountCents: 0,
1430
+ ...firstPaid?.stripePriceId ? { stripePriceId: firstPaid.stripePriceId } : {},
1431
+ ...document.policy ?? {},
1432
+ ...document.scheduling ? { schedulingJson: document.scheduling } : {}
1433
+ };
1434
+ }
1435
+ var byPublicId = (ns, value) => ({
1436
+ ns,
1437
+ attr: "id",
1438
+ value
1439
+ });
1440
+ async function materializeSignupControl(db, envelope, previous, receivedAt) {
1441
+ const { document, digest } = envelope;
1442
+ const currentRows = (await db.query({ tiers: { $: {
1443
+ where: { groupId: document.target.chapterId },
1444
+ limit: 100
1445
+ } } })).tiers ?? [];
1446
+ const incoming = new Set(document.tiers.map((tier) => tier.id));
1447
+ const ops = [];
1448
+ if (previous?.active) ops.push({ t: "update", ns: "signupControls", id: previous.id, attrs: { active: false } });
1449
+ for (const row of currentRows) {
1450
+ const id2 = String(row.id ?? "");
1451
+ if (id2 && !incoming.has(id2)) {
1452
+ ops.push({ t: "update", ns: "tiers", id: byPublicId("tiers", id2), attrs: { active: false } });
1453
+ }
1454
+ }
1455
+ for (const tier of document.tiers) {
1456
+ const tierRef = byPublicId("tiers", tier.id);
1457
+ ops.push({ t: "update", ns: "tiers", id: tierRef, attrs: {
1458
+ id: tier.id,
1459
+ groupId: document.target.chapterId,
1460
+ name: tier.name,
1461
+ priceCents: tier.priceCents,
1462
+ ...tier.stripePriceId ? { stripePriceId: tier.stripePriceId } : {},
1463
+ ...tier.blurb ? { blurb: tier.blurb } : {},
1464
+ sortOrder: tier.sortOrder,
1465
+ active: tier.available,
1466
+ createdAt: receivedAt
1467
+ } });
1468
+ const retract = [
1469
+ ...!tier.stripePriceId ? ["stripePriceId"] : [],
1470
+ ...!tier.blurb ? ["blurb"] : []
1471
+ ];
1472
+ if (retract.length) ops.push({ t: "retract", ns: "tiers", id: tierRef, attrs: retract });
1473
+ }
1474
+ const groupRef = byPublicId("groups", document.target.chapterId);
1475
+ const attrs = groupAttrs(document);
1476
+ ops.push({
1477
+ t: "update",
1478
+ ns: "groups",
1479
+ id: groupRef,
1480
+ attrs
1481
+ });
1482
+ if (!("stripePriceId" in attrs)) {
1483
+ ops.push({ t: "retract", ns: "groups", id: groupRef, attrs: ["stripePriceId"] });
1484
+ }
1485
+ const id = `signup-control:${document.target.environment}:${document.revision}`;
1486
+ ops.push({ t: "update", ns: "signupControls", id, attrs: {
1487
+ id,
1488
+ sourceId: document.source.appId,
1489
+ targetId: document.target.chapterId,
1490
+ environment: document.target.environment,
1491
+ revision: document.revision,
1492
+ digest,
1493
+ document,
1494
+ active: true,
1495
+ receivedAt
1496
+ } });
1497
+ await db.transact(ops, { mutationId: `signup-control:${document.target.environment}:${document.revision}:${digest}` });
1498
+ }
1499
+
1500
+ // src/session.ts
1501
+ function applicationSummary(app) {
1502
+ return {
1503
+ id: app.id,
1504
+ firstName: app.firstName ?? null,
1505
+ lastName: app.lastName ?? null,
1506
+ email: app.email ?? null,
1507
+ status: app.status,
1508
+ createdAt: app.createdAt ?? null,
1509
+ meetingLink: app.meetingLink ?? null,
1510
+ paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
1511
+ renewalAt: app.renewalAt ?? null,
1512
+ canceled: app.canceled === true
1513
+ };
1514
+ }
1515
+ function memberApplication(app, meeting, defaultTimezone) {
1516
+ const summary = applicationSummary(app);
1517
+ let meetingAt = app.meetingAt ?? null;
1518
+ let meetUrl = null;
1519
+ let timezone = defaultTimezone;
1520
+ if (meeting) {
1521
+ timezone = meeting.timezone ?? timezone;
1522
+ if (meeting.status === "scheduled") {
1523
+ meetingAt = meeting.startAt ?? null;
1524
+ meetUrl = meeting.meetUrl ?? null;
1525
+ } else {
1526
+ meetingAt = null;
1527
+ }
1528
+ }
1529
+ return { ...summary, meetingAt, meetUrl, timezone };
1530
+ }
1531
+
1532
+ // src/clerk.ts
1533
+ var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
1534
+ function clerkInviteRequest(input) {
1535
+ return {
1536
+ path: "/v1/invitations",
1537
+ body: {
1538
+ email_address: input.email,
1539
+ notify: true,
1540
+ ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
1541
+ ...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
1542
+ }
1041
1543
  };
1042
- await deps.db.transact([{ t: "update", ns: "emailLog", id, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
1043
- return error ? { sent: false, reason: error } : { sent: true };
1044
1544
  }
1045
- function emailGroupFrom(row) {
1046
- const str3 = (v) => typeof v === "string" ? v : void 0;
1047
- const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
1545
+ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
1546
+ const { path, body } = clerkInviteRequest(input);
1547
+ const res = await fetchImpl(`https://api.clerk.com${path}`, {
1548
+ method: "POST",
1549
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
1550
+ body: JSON.stringify(body)
1551
+ });
1552
+ return res.ok ? { ok: true, status: res.status } : heal(res.status);
1553
+ }
1554
+ function clerkUserRequest(input) {
1048
1555
  return {
1049
- id: String(row.id),
1050
- name: String(row.name ?? ""),
1051
- replyTo: str3(row.replyTo) ?? "",
1052
- debugEmail: str3(row.debugEmail),
1053
- refundPolicyText: str3(row.refundPolicyText),
1054
- commitmentText: str3(row.commitmentText),
1055
- normsText: str3(row.normsText),
1056
- emailTemplates: templates
1556
+ path: "/v1/users",
1557
+ body: {
1558
+ email_address: [input.email],
1559
+ skip_password_requirement: true,
1560
+ ...input.firstName ? { first_name: input.firstName } : {},
1561
+ ...input.lastName ? { last_name: input.lastName } : {},
1562
+ ...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
1563
+ ...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
1564
+ }
1565
+ };
1566
+ }
1567
+ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fetch) {
1568
+ const res = await fetchImpl(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}/metadata`, {
1569
+ method: "PATCH",
1570
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
1571
+ body: JSON.stringify({
1572
+ ...input.publicMetadata !== void 0 ? { public_metadata: input.publicMetadata } : {},
1573
+ ...input.privateMetadata !== void 0 ? { private_metadata: input.privateMetadata } : {}
1574
+ })
1575
+ });
1576
+ return res.ok;
1577
+ }
1578
+ async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
1579
+ const auth = { authorization: `Bearer ${secretKey}` };
1580
+ const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
1581
+ if (!found.ok) return false;
1582
+ const users = await found.json().catch(() => null);
1583
+ const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
1584
+ if (!id) return false;
1585
+ return updateClerkUserMetadata(secretKey, id, input, fetchImpl);
1586
+ }
1587
+ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
1588
+ const { path, body } = clerkUserRequest(input);
1589
+ const res = await fetchImpl(`https://api.clerk.com${path}`, {
1590
+ method: "POST",
1591
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
1592
+ body: JSON.stringify(body)
1593
+ });
1594
+ if (res.ok) return { ok: true, status: res.status };
1595
+ const healed = heal(res.status);
1596
+ const repair = input.repairMetadata ?? {
1597
+ ...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
1598
+ ...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
1057
1599
  };
1600
+ if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
1601
+ const refreshed = await updateClerkUserMetadataByEmail(secretKey, input.email, {
1602
+ ...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
1603
+ ...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
1604
+ }, fetchImpl).catch(() => false);
1605
+ return { ...healed, refreshed };
1058
1606
  }
1059
1607
 
1060
1608
  // src/worker-provisioning.ts
@@ -1184,8 +1732,12 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
1184
1732
 
1185
1733
  // src/worker-routes.ts
1186
1734
  async function memberSessionApplication(db, chapterId, email) {
1187
- const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: "desc" }, limit: 1 } } })).applications;
1188
- const app = Array.isArray(apps) ? apps[0] : void 0;
1735
+ let apps = (await db.query({ applications: { $: {
1736
+ where: { emailNormalized: normalizeAdmissionEmail(email) },
1737
+ limit: 20
1738
+ } } })).applications;
1739
+ if (!apps?.length) apps = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
1740
+ const app = Array.isArray(apps) ? [...apps].sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0] : void 0;
1189
1741
  if (!app) return null;
1190
1742
  const meetings = (await db.query({ meetings: { $: { where: { applicationId: app.id, status: "scheduled" }, order: { createdAt: "desc" }, limit: 1 } } })).meetings;
1191
1743
  const meeting = Array.isArray(meetings) ? meetings[0] : void 0;
@@ -1241,7 +1793,7 @@ var handleCrm = async (req, url, env, ctx) => {
1241
1793
  const crmBase = ctx.crmBase;
1242
1794
  if (url.pathname !== crmBase && !url.pathname.startsWith(crmBase + "/")) return null;
1243
1795
  const db = ctx.makeDb(env);
1244
- const routes = (0, import_crm3.createCrmRoutes)({
1796
+ const routes = (0, import_crm4.createCrmRoutes)({
1245
1797
  crm: ctx.chapter.crm,
1246
1798
  db,
1247
1799
  authorize: async (r) => {
@@ -1287,7 +1839,8 @@ var handleMember = async (req, url, env, ctx) => {
1287
1839
  membershipPage: activeControl.document.membershipPage,
1288
1840
  application: activeControl.document.application,
1289
1841
  theme: activeControl.document.theme
1290
- } } : {}
1842
+ } } : {},
1843
+ giftOffers: publicGiftOffers(activeControl?.document.giftOffers, Date.now())
1291
1844
  });
1292
1845
  }
1293
1846
  if (req.method === "GET" && url.pathname === "/api/join/resume") {
@@ -1326,18 +1879,9 @@ var handleMember = async (req, url, env, ctx) => {
1326
1879
  })).groups;
1327
1880
  const group = Array.isArray(groups) ? groups[0] : void 0;
1328
1881
  if (!group) return json({ error: "not found" }, 404);
1329
- const stripeKey = await getVaultSecret(db, "stripe_secret_key");
1330
- const status = String(app.status ?? "");
1331
- const offered = await loadOfferableTiers(db, group, Boolean(stripeKey));
1332
- const tier = findTier(offered, typeof app.tierId === "string" ? app.tierId : null);
1333
- const payable = Boolean(tier) && !tierIsFree(tier) && tierPayable(tier, group, Boolean(stripeKey));
1334
- if (!payable) return json({ state: "booking", applicationId });
1335
- if (status === chapter.pipeline.initial && app.stripeSubscriptionId) {
1336
- return json({ state: "paymentPending", applicationId });
1337
- }
1338
- if (status === chapter.pipeline.initial) return json({ state: "payment", applicationId });
1339
- if (canBook(status, chapter.pipeline)) return json({ state: "booking", applicationId });
1340
- return json({ error: `application cannot resume from status "${status}"` }, 409);
1882
+ const state = await continueJoin(db, ctx, env, url.origin, app, group);
1883
+ if (state) return json({ state, applicationId, ...state === "gift" ? { claimId: app.giftClaimId } : {} });
1884
+ return json({ error: `application cannot resume from status "${String(app.status ?? "")}"` }, 409);
1341
1885
  }
1342
1886
  if (req.method === "POST" && url.pathname === "/api/applications") {
1343
1887
  const raw = await req.text();
@@ -1353,12 +1897,19 @@ var handleMember = async (req, url, env, ctx) => {
1353
1897
  const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
1354
1898
  const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
1355
1899
  const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
1900
+ const now = Date.now();
1901
+ const requestedGiftId = typeof parsed.giftOfferId === "string" ? parsed.giftOfferId : "";
1902
+ const giftOffer = requestedGiftId ? findGiftOffer(activeControl?.document.giftOffers, requestedGiftId, now) : null;
1903
+ if (requestedGiftId && !giftOffer) return json({ error: "gift offer is unavailable" }, 400);
1904
+ const effectiveGrant = !giftOffer && typeof parsed.email === "string" ? await findAdmissionGrant(db, chapter.id, parsed.email, now) : null;
1356
1905
  const result = await submitApplication(db, effectiveChapter, parsed, {
1357
1906
  submissionId,
1358
1907
  groupId: chapter.id,
1359
- now: Date.now(),
1908
+ now,
1360
1909
  newId: () => crypto.randomUUID(),
1361
- ...offeredTierIds ? { tierIds: offeredTierIds } : {}
1910
+ ...offeredTierIds ? { tierIds: offeredTierIds } : {},
1911
+ ...effectiveGrant?.decision === "waive_interview" ? { admissionGrant: effectiveGrant } : {},
1912
+ ...giftOffer ? { giftOfferId: giftOffer.id } : {}
1362
1913
  });
1363
1914
  if (!result.ok) return json({ error: result.error }, 400);
1364
1915
  if (!result.duplicate) {
@@ -1367,10 +1918,16 @@ var handleMember = async (req, url, env, ctx) => {
1367
1918
  }
1368
1919
  await provisionApplicant(db, effectiveChapter, result.id, parsed);
1369
1920
  }
1921
+ const apps = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications;
1922
+ const canonical2 = Array.isArray(apps) ? apps[0] : void 0;
1923
+ const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
1924
+ const group = Array.isArray(groups) ? groups[0] : void 0;
1925
+ const state = canonical2 && group ? await continueJoin(db, ctx, env, url.origin, canonical2, group) : null;
1370
1926
  return json({
1371
1927
  id: result.id,
1372
1928
  duplicate: result.duplicate,
1373
1929
  status: result.status,
1930
+ ...state ? { state } : {},
1374
1931
  // Echoed so a site can see (and assert in an integration test) whether its
1375
1932
  // join page actually posted the ack. Absent consent is otherwise invisible.
1376
1933
  disclaimerAckAt: result.disclaimerAckAt
@@ -1426,8 +1983,8 @@ var handleNetworkShared = async (req, url, env, ctx) => {
1426
1983
  payload,
1427
1984
  sourceId ? { sourceId } : {}
1428
1985
  );
1429
- const record = normalizeSharedRecord(payload, sourceId);
1430
- return json({ recordId, type: record.type });
1986
+ const record2 = normalizeSharedRecord(payload, sourceId);
1987
+ return json({ recordId, type: record2.type });
1431
1988
  } catch (error) {
1432
1989
  return json({
1433
1990
  error: error instanceof Error ? error.message : "invalid shared record"
@@ -1438,29 +1995,6 @@ var handleNetworkShared = async (req, url, env, ctx) => {
1438
1995
  // src/worker-routes-signup-control.ts
1439
1996
  var import_db3 = require("@odla-ai/db");
1440
1997
 
1441
- // src/signup-control-canonical.ts
1442
- var encoder = new TextEncoder();
1443
- function canonicalValue(value) {
1444
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
1445
- return JSON.stringify(value);
1446
- }
1447
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
1448
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
1449
- const record = value;
1450
- const entries = Object.keys(record).sort().flatMap((key) => {
1451
- const item = record[key];
1452
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
1453
- });
1454
- return `{${entries.join(",")}}`;
1455
- }
1456
- function canonicalSignupControl(value) {
1457
- return canonicalValue(value);
1458
- }
1459
- async function signupControlDigest(document) {
1460
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
1461
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
1462
- }
1463
-
1464
1998
  // src/signup-control-contract.ts
1465
1999
  var SIGNUP_CONTROL_PATH = "/api/signup-control";
1466
2000
 
@@ -1504,10 +2038,10 @@ function boundedString(value, path, errors, max, required = false) {
1504
2038
  errors.push(`${path} must be a string`);
1505
2039
  return void 0;
1506
2040
  }
1507
- const text2 = value.trim();
1508
- if (required && !text2) errors.push(`${path} is required`);
1509
- if (text2.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
1510
- return text2;
2041
+ const text3 = value.trim();
2042
+ if (required && !text3) errors.push(`${path} is required`);
2043
+ if (text3.length > max) errors.push(`${path} must be at most ${max.toLocaleString("en-US")} characters`);
2044
+ return text3;
1511
2045
  }
1512
2046
  function validateMembershipPageDocument(input, options = {}) {
1513
2047
  const errors = [];
@@ -1556,8 +2090,8 @@ function validateMembershipPageDocument(input, options = {}) {
1556
2090
  else {
1557
2091
  if (item.benefits.length > MAX_BENEFITS) errors.push(`${path}.benefits supports at most ${MAX_BENEFITS} entries`);
1558
2092
  benefits = item.benefits.slice(0, MAX_BENEFITS).flatMap((benefit, benefitIndex) => {
1559
- const text2 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
1560
- return text2 ? [text2] : [];
2093
+ const text3 = boundedString(benefit, `${path}.benefits[${benefitIndex}]`, errors, 2e3, true);
2094
+ return text3 ? [text3] : [];
1561
2095
  });
1562
2096
  }
1563
2097
  }
@@ -1631,7 +2165,7 @@ function validateMembershipPageDocument(input, options = {}) {
1631
2165
  var SLUG = /^[a-z0-9][a-z0-9-]{1,62}$/;
1632
2166
  var ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
1633
2167
  var TIER = /^[a-z][a-z0-9-]{0,79}$/;
1634
- var PRICE = /^price_[A-Za-z0-9]{12,}$/;
2168
+ var PRICE2 = /^price_[A-Za-z0-9]{12,}$/;
1635
2169
  var ROOT_KEYS2 = /* @__PURE__ */ new Set([
1636
2170
  "schemaVersion",
1637
2171
  "revision",
@@ -1643,14 +2177,15 @@ var ROOT_KEYS2 = /* @__PURE__ */ new Set([
1643
2177
  "application",
1644
2178
  "policy",
1645
2179
  "scheduling",
1646
- "theme"
2180
+ "theme",
2181
+ "giftOffers"
1647
2182
  ]);
1648
2183
  var TIER_KEYS2 = /* @__PURE__ */ new Set(["id", "name", "available", "sortOrder", "priceCents", "stripePriceId", "blurb"]);
1649
2184
  var recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1650
2185
  function unknownKeys(value, allowed, path, errors) {
1651
2186
  for (const key of Object.keys(value)) if (!allowed.has(key)) errors.push(`${path}.${key} is not supported`);
1652
2187
  }
1653
- function text(value, path, errors, max = 65536) {
2188
+ function text2(value, path, errors, max = 65536) {
1654
2189
  if (typeof value !== "string" || !value.trim()) {
1655
2190
  errors.push(`${path} is required`);
1656
2191
  return "";
@@ -1673,23 +2208,23 @@ function tiersOf(value, errors) {
1673
2208
  return [];
1674
2209
  }
1675
2210
  unknownKeys(item, TIER_KEYS2, path, errors);
1676
- const id = text(item.id, `${path}.id`, errors, 80);
2211
+ const id = text2(item.id, `${path}.id`, errors, 80);
1677
2212
  if (id && !TIER.test(id)) errors.push(`${path}.id must be a lowercase kebab-case id`);
1678
2213
  if (seen.has(id)) errors.push(`${path}.id duplicates "${id}"`);
1679
2214
  seen.add(id);
1680
- const name = text(item.name, `${path}.name`, errors, 160);
2215
+ const name = text2(item.name, `${path}.name`, errors, 160);
1681
2216
  if (typeof item.available !== "boolean") errors.push(`${path}.available must be boolean`);
1682
2217
  if (!Number.isInteger(item.sortOrder) || Number(item.sortOrder) < 0) errors.push(`${path}.sortOrder must be a non-negative integer`);
1683
2218
  if (!Number.isInteger(item.priceCents) || Number(item.priceCents) < 0 || Number(item.priceCents) > 1e8) {
1684
2219
  errors.push(`${path}.priceCents must be an integer from 0 to 100,000,000`);
1685
2220
  }
1686
- if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE.test(item.stripePriceId))) {
2221
+ if (item.stripePriceId !== void 0 && (typeof item.stripePriceId !== "string" || !PRICE2.test(item.stripePriceId))) {
1687
2222
  errors.push(`${path}.stripePriceId must be a real Stripe Price id`);
1688
2223
  }
1689
2224
  if (item.available === true && Number(item.priceCents) > 0 && !item.stripePriceId) {
1690
2225
  errors.push(`${path}.stripePriceId is required for an available paid tier`);
1691
2226
  }
1692
- const blurb = item.blurb === void 0 ? void 0 : text(item.blurb, `${path}.blurb`, errors, 4e3);
2227
+ const blurb = item.blurb === void 0 ? void 0 : text2(item.blurb, `${path}.blurb`, errors, 4e3);
1693
2228
  return [{
1694
2229
  id,
1695
2230
  name,
@@ -1738,7 +2273,7 @@ function policyOf(value, errors) {
1738
2273
  const result = {};
1739
2274
  for (const key of allowed) {
1740
2275
  if (input[key] === void 0 && key !== "disclaimerText" && key !== "refundPolicyText") continue;
1741
- result[key] = text(input[key], `document.policy.${key}`, errors);
2276
+ result[key] = text2(input[key], `document.policy.${key}`, errors);
1742
2277
  }
1743
2278
  return result;
1744
2279
  }
@@ -1776,9 +2311,9 @@ function validateSignupControlDocument(input) {
1776
2311
  if (!Number.isSafeInteger(root.generatedAt) || Number(root.generatedAt) < 1) errors.push("document.generatedAt must be epoch milliseconds");
1777
2312
  const source = recordOf(root.source);
1778
2313
  const target = recordOf(root.target);
1779
- const appId = text(source?.appId, "document.source.appId", errors, 63);
1780
- const chapterId = text(target?.chapterId, "document.target.chapterId", errors, 63);
1781
- const environment = text(target?.environment, "document.target.environment", errors, 63);
2314
+ const appId = text2(source?.appId, "document.source.appId", errors, 63);
2315
+ const chapterId = text2(target?.chapterId, "document.target.chapterId", errors, 63);
2316
+ const environment = text2(target?.environment, "document.target.environment", errors, 63);
1782
2317
  if (appId && !SLUG.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
1783
2318
  if (chapterId && !SLUG.test(chapterId)) errors.push("document.target.chapterId must be a lowercase app slug");
1784
2319
  if (environment && !ENVIRONMENT.test(environment)) errors.push("document.target.environment is invalid");
@@ -1799,6 +2334,8 @@ function validateSignupControlDocument(input) {
1799
2334
  else scheduling = root.scheduling;
1800
2335
  }
1801
2336
  const theme = themeOf(root.theme, errors);
2337
+ const giftOffers = root.giftOffers === void 0 ? void 0 : validateGiftOfferCatalog(root.giftOffers, new Set(tiers.map((tier) => tier.id)));
2338
+ if (giftOffers && !giftOffers.ok) errors.push(...giftOffers.errors);
1802
2339
  if (errors.length || !page.ok) return { ok: false, errors };
1803
2340
  return { ok: true, value: {
1804
2341
  schemaVersion: 1,
@@ -1811,7 +2348,8 @@ function validateSignupControlDocument(input) {
1811
2348
  ...application ? { application } : {},
1812
2349
  ...policy ? { policy } : {},
1813
2350
  ...scheduling ? { scheduling } : {},
1814
- ...theme ? { theme } : {}
2351
+ ...theme ? { theme } : {},
2352
+ ...giftOffers?.ok ? { giftOffers: giftOffers.value } : {}
1815
2353
  } };
1816
2354
  }
1817
2355
 
@@ -1872,92 +2410,156 @@ var handleSignupControl = async (req, url, env, ctx) => {
1872
2410
  });
1873
2411
  };
1874
2412
 
1875
- // src/crm-sync.ts
1876
- var import_crm4 = require("@odla-ai/crm");
1877
- var str = (v) => typeof v === "string" ? v : v == null ? "" : String(v);
1878
- var crmDeps = (db, ctx) => ({
1879
- crm: ctx.chapter.crm,
1880
- db,
1881
- now: () => Date.now(),
1882
- newId: () => crypto.randomUUID(),
1883
- chapter: ctx.chapter
1884
- });
1885
- function personInputFromApp(chapter, app) {
1886
- const input = sharedPersonInput({
1887
- email: str(app.email),
1888
- firstName: str(app.firstName) || void 0,
1889
- lastName: str(app.lastName) || void 0,
1890
- phone: str(app.phone) || void 0,
1891
- linkedin: str(app.linkedin) || void 0,
1892
- hubRecordId: str(app.id)
1893
- });
1894
- for (const f of chapter.application.crmFields) {
1895
- if (app[f] !== void 0) input[f] = app[f];
2413
+ // src/worker-routes-admission-grants.ts
2414
+ var import_db4 = require("@odla-ai/db");
2415
+ var reject2 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
2416
+ var handleAdmissionGrants = async (req, url, env, ctx) => {
2417
+ if (req.method !== "POST" || url.pathname !== ADMISSION_GRANTS_PATH) return null;
2418
+ const authority = ctx.chapter.signupControl;
2419
+ if (!authority) return json({ error: "not found" }, 404);
2420
+ const db = ctx.makeDb(env);
2421
+ const secret = await getVaultSecret(db, authority.secretName);
2422
+ if (!secret) return json({ error: "unauthorized" }, 401);
2423
+ const verified = await (0, import_db4.verifyFederatedRequest)(req, { secret, senders: [authority.sourceId] });
2424
+ if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
2425
+ let input;
2426
+ try {
2427
+ input = JSON.parse(verified.body);
2428
+ } catch {
2429
+ return reject2("invalid_document", 400, ["request body must be JSON"]);
1896
2430
  }
1897
- if (app.id !== void 0) input.applicationId = str(app.id);
1898
- return input;
1899
- }
1900
- function billingColumns(app) {
1901
- const status = str(app.status);
1902
- const paid = Boolean(app.stripeSubscriptionId) && status !== "refunded";
1903
- const billingStatus = status === "refunded" ? "refunded" : app.canceled === true ? "canceled" : paid ? "active" : "none";
1904
- const cols = { billingStatus };
1905
- if (app.stripeCustomerId) cols.stripeCustomerId = str(app.stripeCustomerId);
1906
- if (app.stripeSubscriptionId) cols.subscriptionId = str(app.stripeSubscriptionId);
1907
- if (typeof app.renewalAt === "number") cols.renewalAt = app.renewalAt;
1908
- return cols;
1909
- }
1910
- async function syncApplicationToCrm(deps, opts) {
1911
- const emailKey = str(opts.app.email).toLowerCase();
1912
- if (!emailKey) return null;
1913
- const recordDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
1914
- const input = personInputFromApp(deps.chapter, opts.app);
1915
- const { crm_record } = await deps.db.query({
1916
- crm_record: { $: { where: { type: "person", primaryEmail: emailKey }, limit: 1 } }
1917
- });
1918
- const existing = crm_record?.[0] ?? null;
1919
- const stage = opts.stage || void 0;
1920
- let recordId;
1921
- if (existing && typeof existing.id === "string") {
1922
- recordId = existing.id;
1923
- await (0, import_crm4.updateRecord)(recordDeps, { id: recordId, input });
1924
- } else {
1925
- const created = await (0, import_crm4.createRecord)(recordDeps, { type: "person", input, ...stage ? { stage } : {} });
1926
- recordId = created.id;
2431
+ const digest = typeof input.digest === "string" ? input.digest : "";
2432
+ const checked = validateAdmissionGrantDocument(input.document);
2433
+ if (!checked.ok) return reject2("invalid_document", 400, checked.errors);
2434
+ const document = checked.value;
2435
+ if (document.source.appId !== authority.sourceId) return reject2("wrong_source", 403);
2436
+ if (document.target.chapterId !== ctx.chapter.id) return reject2("wrong_target", 409);
2437
+ if (document.target.environment !== env.ODLA_ENV) return reject2("wrong_environment", 409);
2438
+ if (!digest || await signupControlDigest(document) !== digest) return reject2("invalid_digest", 400);
2439
+ const latest = await loadLatestAdmissionRevision(db, authority.sourceId, ctx.chapter.id, env.ODLA_ENV);
2440
+ if (latest && document.revision < latest.revision) return reject2("stale_revision", 409);
2441
+ if (latest && document.revision === latest.revision) {
2442
+ if (latest.digest !== digest) return reject2("conflicting_revision", 409);
2443
+ return json({
2444
+ ok: true,
2445
+ target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
2446
+ revision: latest.revision,
2447
+ digest: latest.digest,
2448
+ status: "replayed",
2449
+ acknowledgedAt: latest.receivedAt
2450
+ });
1927
2451
  }
1928
- if (existing && stage && existing.stage !== stage) {
1929
- await (0, import_crm4.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${stage}` }).catch(() => void 0);
2452
+ const receivedAt = Date.now();
2453
+ try {
2454
+ await materializeAdmissionGrants(db, { digest, document }, latest, receivedAt);
2455
+ } catch {
2456
+ return reject2("materialization_failed", 503);
1930
2457
  }
1931
- await deps.db.transact([{ t: "update", ns: "crm_record", id: recordId, attrs: billingColumns(opts.app) }]);
1932
- await (0, import_crm4.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
1933
- return recordId;
1934
- }
1935
- async function backfillCrm(deps) {
1936
- const [appsRes, usersRes] = await Promise.all([
1937
- deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1938
- deps.db.query({ $users: { $: { limit: 1e3 } } })
1939
- ]);
1940
- const seen = /* @__PURE__ */ new Set();
1941
- let synced = 0;
1942
- const errors = [];
1943
- const run = async (app, stage) => {
1944
- const key = str(app.email).toLowerCase();
1945
- if (!key || seen.has(key)) return;
1946
- seen.add(key);
2458
+ return json({
2459
+ ok: true,
2460
+ target: { chapterId: ctx.chapter.id, environment: env.ODLA_ENV },
2461
+ revision: document.revision,
2462
+ digest,
2463
+ status: "accepted",
2464
+ acknowledgedAt: receivedAt
2465
+ });
2466
+ };
2467
+
2468
+ // src/worker-routes-admin-admission.ts
2469
+ async function gate(req, env, ctx) {
2470
+ const rawDb = ctx.makeDb(env);
2471
+ const user = await ctx.verifyUser(req, env);
2472
+ if (!user) return json({ error: "unauthorized" }, 401);
2473
+ if (!await ctx.isAdmin(rawDb, user)) return json({ error: "forbidden" }, 403);
2474
+ return { db: rawDb, user };
2475
+ }
2476
+ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
2477
+ if (url.pathname === "/api/admin/admission-grants" && req.method === "GET") {
2478
+ const auth = await gate(req, env, ctx);
2479
+ if (auth instanceof Response) return auth;
2480
+ const result = await auth.db.query({ admissionGrants: { $: {
2481
+ where: { targetId: ctx.chapter.id },
2482
+ order: { createdAt: "desc" },
2483
+ limit: 500
2484
+ } } });
2485
+ const grants = Array.isArray(result.admissionGrants) ? result.admissionGrants.map((row) => ({ ...row, emailNormalized: void 0 })) : [];
2486
+ return json({ grants });
2487
+ }
2488
+ if (url.pathname === "/api/admin/admission-grants" && req.method === "POST") {
2489
+ const auth = await gate(req, env, ctx);
2490
+ if (auth instanceof Response) return auth;
2491
+ let body;
1947
2492
  try {
1948
- await syncApplicationToCrm(deps, { app, stage });
1949
- synced += 1;
1950
- } catch (err) {
1951
- errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
2493
+ body = JSON.parse(await req.text());
2494
+ } catch {
2495
+ return json({ error: "invalid JSON body" }, 400);
2496
+ }
2497
+ const email = typeof body.email === "string" ? body.email.trim() : "";
2498
+ const emailNormalized = normalizeAdmissionEmail(email);
2499
+ if (!isValidEmail(emailNormalized)) return json({ error: "email must be valid" }, 400);
2500
+ const decision = body.decision ?? "waive_interview";
2501
+ if (decision !== "waive_interview" && decision !== "require_interview") {
2502
+ return json({ error: "decision must be waive_interview or require_interview" }, 400);
2503
+ }
2504
+ const note = typeof body.note === "string" ? body.note.trim() : "";
2505
+ if (note.length > 2e3) return json({ error: "note is too long" }, 400);
2506
+ const expiresAt = body.expiresAt === void 0 || body.expiresAt === null ? void 0 : Number(body.expiresAt);
2507
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= Date.now())) {
2508
+ return json({ error: "expiresAt must be a future epoch timestamp" }, 400);
2509
+ }
2510
+ const sourceGrantId = crypto.randomUUID();
2511
+ const id = `admission:local:${ctx.chapter.id}:${sourceGrantId}`;
2512
+ const createdAt = Date.now();
2513
+ const actor = auth.user.email ?? auth.user.userId;
2514
+ await auth.db.transact([{ t: "update", ns: "admissionGrants", id, attrs: {
2515
+ id,
2516
+ sourceGrantId,
2517
+ sourceScope: "local",
2518
+ sourceId: ctx.chapter.id,
2519
+ targetId: ctx.chapter.id,
2520
+ email,
2521
+ emailNormalized,
2522
+ decision,
2523
+ ...note ? { note } : {},
2524
+ ...expiresAt !== void 0 ? { expiresAt } : {},
2525
+ active: true,
2526
+ createdAt,
2527
+ createdBy: actor,
2528
+ updatedAt: createdAt,
2529
+ updatedBy: actor
2530
+ } }], { mutationId: `admission-local-create:${sourceGrantId}` });
2531
+ return json({ grant: { id, sourceGrantId, sourceScope: "local", email, decision, active: true, createdAt, ...note ? { note } : {}, ...expiresAt ? { expiresAt } : {} } }, 201);
2532
+ }
2533
+ const prefix = "/api/admin/admission-grants/";
2534
+ let revokeId = "";
2535
+ if (url.pathname.startsWith(prefix)) {
2536
+ try {
2537
+ revokeId = decodeURIComponent(url.pathname.slice(prefix.length));
2538
+ } catch {
2539
+ revokeId = "";
1952
2540
  }
1953
- };
1954
- for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1955
- for (const u of usersRes.$users ?? []) {
1956
- if (u.deleted === true) continue;
1957
- await run({ email: u.email, firstName: str(u.name) });
1958
2541
  }
1959
- return { synced, errors };
1960
- }
2542
+ if (/^admission:local:[a-z0-9-]+:[0-9a-f-]+$/.test(revokeId) && req.method === "DELETE") {
2543
+ const auth = await gate(req, env, ctx);
2544
+ if (auth instanceof Response) return auth;
2545
+ const id = revokeId;
2546
+ const found = await auth.db.query({ admissionGrants: { $: { where: { id }, limit: 1 } } });
2547
+ const row = Array.isArray(found.admissionGrants) ? found.admissionGrants[0] : void 0;
2548
+ if (!row || row.targetId !== ctx.chapter.id || row.sourceScope !== "local") return json({ error: "not found" }, 404);
2549
+ if (row.active !== true) return json({ ok: true, replayed: true });
2550
+ const revokedAt = Date.now();
2551
+ const actor = auth.user.email ?? auth.user.userId;
2552
+ await auth.db.transact([{ t: "update", ns: "admissionGrants", id, attrs: {
2553
+ active: false,
2554
+ revokedAt,
2555
+ revokedBy: actor,
2556
+ updatedAt: revokedAt,
2557
+ updatedBy: actor
2558
+ } }], { mutationId: `admission-local-revoke:${id}` });
2559
+ return json({ ok: true, replayed: false });
2560
+ }
2561
+ return null;
2562
+ };
1961
2563
 
1962
2564
  // src/worker-routes-schedule.ts
1963
2565
  var import_calendar = require("@odla-ai/calendar");
@@ -1973,8 +2575,8 @@ function makeCalendar(env) {
1973
2575
  }
1974
2576
  async function firstRow(db, ns, q) {
1975
2577
  const res = await db.query({ [ns]: { $: q } });
1976
- const rows2 = res[ns];
1977
- return Array.isArray(rows2) ? rows2[0] : void 0;
2578
+ const rows4 = res[ns];
2579
+ return Array.isArray(rows4) ? rows4[0] : void 0;
1978
2580
  }
1979
2581
  async function computeSlots(cal, cfg) {
1980
2582
  const { from, to } = slotWindow(Date.now(), cfg.windowDays);
@@ -2111,8 +2713,26 @@ function findApplicationRef(obj) {
2111
2713
  const customerId = typeof obj.customer === "string" ? obj.customer : void 0;
2112
2714
  return { ...applicationId ? { applicationId } : {}, ...customerId ? { customerId } : {} };
2113
2715
  }
2716
+ function findGiftRef(obj) {
2717
+ const metaOf = (value) => value && typeof value === "object" ? value.metadata ?? {} : {};
2718
+ const pick = (metadata) => typeof metadata.giftIntentId === "string" ? metadata.giftIntentId : void 0;
2719
+ const parent = obj.parent;
2720
+ const subscription = obj.subscription_details ?? parent?.subscription_details;
2721
+ const giftIntentId = pick(metaOf(obj)) ?? pick(metaOf(subscription));
2722
+ const subscriptionId = typeof obj.subscription === "string" ? obj.subscription : typeof subscription?.subscription === "string" ? String(subscription.subscription) : void 0;
2723
+ return { ...giftIntentId ? { giftIntentId } : {}, providerObjectId: subscriptionId ?? String(obj.id ?? "") };
2724
+ }
2114
2725
  function normalizeWebhookEvent(event) {
2115
2726
  const obj = event.data?.object ?? {};
2727
+ const gift = findGiftRef(obj);
2728
+ if (gift.giftIntentId) {
2729
+ if (event.type === "payment_intent.succeeded" || event.type === "invoice.paid" && obj.billing_reason === "subscription_create") {
2730
+ return { kind: "gift_paid", giftIntentId: gift.giftIntentId, providerObjectId: gift.providerObjectId };
2731
+ }
2732
+ if (event.type === "charge.refunded") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "refunded" };
2733
+ if (event.type === "customer.subscription.deleted") return { kind: "gift_revoked", giftIntentId: gift.giftIntentId, reason: "canceled" };
2734
+ return { kind: "ignored", type: event.type };
2735
+ }
2116
2736
  const ref = findApplicationRef(obj);
2117
2737
  switch (event.type) {
2118
2738
  case "invoice.paid": {
@@ -2213,9 +2833,9 @@ function createStripeProvider(config) {
2213
2833
  `sub:${input.applicationId}`
2214
2834
  );
2215
2835
  if (!sub.ok) fail("subscription create", sub);
2216
- const clientSecret = clientSecretOf(sub.body);
2217
- if (!clientSecret) fail("subscription confirmation-secret missing", sub);
2218
- return { customerId, subscriptionId: String(sub.body.id), clientSecret };
2836
+ const clientSecret2 = clientSecretOf(sub.body);
2837
+ if (!clientSecret2) fail("subscription confirmation-secret missing", sub);
2838
+ return { customerId, subscriptionId: String(sub.body.id), clientSecret: clientSecret2 };
2219
2839
  },
2220
2840
  async ingestWebhook(rawBody, sigHeader) {
2221
2841
  if (!webhookSecret || !await (0, import_stripe2.verifyStripeSignature)(rawBody, sigHeader, webhookSecret)) {
@@ -2228,8 +2848,8 @@ function createStripeProvider(config) {
2228
2848
  const sk = requireSecret(secretKey);
2229
2849
  const charges = await stripeCall(sk, "GET", "/v1/charges", { customer: input.customerId, limit: 100 });
2230
2850
  if (!charges.ok) fail("charges list", charges);
2231
- const rows2 = charges.body.data ?? [];
2232
- const paid = rows2.filter((c) => c.status === "succeeded" && c.refunded !== true);
2851
+ const rows4 = charges.body.data ?? [];
2852
+ const paid = rows4.filter((c) => c.status === "succeeded" && c.refunded !== true);
2233
2853
  const charge = paid[paid.length - 1];
2234
2854
  if (!charge) {
2235
2855
  const err = new Error("no paid charge to refund");
@@ -2245,12 +2865,337 @@ function createStripeProvider(config) {
2245
2865
  };
2246
2866
  }
2247
2867
 
2868
+ // src/gift-codes.ts
2869
+ var GIFT_CODE_VERSION = 1;
2870
+ var GIFT_CODE_BYTES = 32;
2871
+ var base64url = (bytes) => {
2872
+ let binary = "";
2873
+ for (const byte of bytes) binary += String.fromCharCode(byte);
2874
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
2875
+ };
2876
+ function createGiftCode() {
2877
+ const bytes = new Uint8Array(GIFT_CODE_BYTES);
2878
+ crypto.getRandomValues(bytes);
2879
+ return `gift${GIFT_CODE_VERSION}_${base64url(bytes)}`;
2880
+ }
2881
+ async function hashGiftCode(chapterId, code, version = GIFT_CODE_VERSION) {
2882
+ const bytes = new TextEncoder().encode(`odla-gift:${version}:${chapterId}:${code}`);
2883
+ return base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
2884
+ }
2885
+ async function giftRequesterBucket(chapterId, requester) {
2886
+ const bytes = new TextEncoder().encode(`odla-gift-rate:${chapterId}:${requester}`);
2887
+ return base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))).slice(0, 32);
2888
+ }
2889
+
2890
+ // src/gift-store.ts
2891
+ var first = (value) => Array.isArray(value) ? value[0] : void 0;
2892
+ async function prepareGiftCode(chapterId) {
2893
+ const code = createGiftCode();
2894
+ return { code, codeHash: await hashGiftCode(chapterId, code), codeVersion: GIFT_CODE_VERSION };
2895
+ }
2896
+ async function issueGiftClaim(db, intent, provider, now) {
2897
+ const existing = first((await db.query({ giftClaims: { $: { where: { intentId: intent.id }, limit: 1 } } })).giftClaims);
2898
+ if (existing) return { claimId: String(existing.id), orderId: String(existing.orderId), duplicate: true };
2899
+ const offer = intent.offerSnapshot;
2900
+ const orderId = `gift-order:${intent.id}`;
2901
+ const claimId = `gift-claim:${intent.id}`;
2902
+ const shared = {
2903
+ chapterId: intent.chapterId,
2904
+ offerId: intent.offerId,
2905
+ ...intent.purchaserApplicationId ? { purchaserApplicationId: intent.purchaserApplicationId } : {},
2906
+ ...intent.purchaserUserId ? { purchaserUserId: intent.purchaserUserId } : {},
2907
+ purchaserEmailNormalized: intent.purchaserEmailNormalized
2908
+ };
2909
+ const ops = [
2910
+ { t: "update", ns: "giftOrders", id: orderId, attrs: {
2911
+ id: orderId,
2912
+ intentId: intent.id,
2913
+ ...shared,
2914
+ environment: intent.environment,
2915
+ offerSnapshot: offer,
2916
+ purchaserType: intent.purchaserType,
2917
+ providerEventId: provider.eventId,
2918
+ providerObjectId: provider.objectId,
2919
+ amountCents: offer.priceCents,
2920
+ currency: offer.currency,
2921
+ billingMode: offer.billingMode,
2922
+ createdAt: now
2923
+ } },
2924
+ { t: "update", ns: "giftClaims", id: claimId, attrs: {
2925
+ id: claimId,
2926
+ orderId,
2927
+ intentId: intent.id,
2928
+ ...shared,
2929
+ target: offer.target,
2930
+ codeHash: intent.codeHash,
2931
+ codeVersion: intent.codeVersion,
2932
+ rotationCount: 0,
2933
+ codeIssuedAt: intent.codeIssuedAt,
2934
+ codeExpiresAt: now + offer.redemptionLifetimeHours * 36e5,
2935
+ attemptCount: 0,
2936
+ attemptWindowStartedAt: now,
2937
+ status: "pending",
2938
+ createdAt: now
2939
+ } },
2940
+ { t: "update", ns: "giftPurchaseIntents", id: intent.id, attrs: { status: "paid", paidAt: now, claimId, updatedAt: now } }
2941
+ ];
2942
+ if (intent.purchaserApplicationId) ops.push({
2943
+ t: "update",
2944
+ ns: "applications",
2945
+ id: intent.purchaserApplicationId,
2946
+ attrs: { giftPaidAt: now, giftClaimId: claimId }
2947
+ });
2948
+ try {
2949
+ const result = await db.transact(ops, {
2950
+ mutationId: `gift-paid:${provider.eventId}`,
2951
+ guards: [{ ns: "giftPurchaseIntents", id: intent.id, exists: true, equals: { status: "checkout_ready" } }]
2952
+ });
2953
+ return { claimId, orderId, duplicate: result.duplicate };
2954
+ } catch {
2955
+ const raced = first((await db.query({ giftClaims: { $: { where: { intentId: intent.id }, limit: 1 } } })).giftClaims);
2956
+ if (raced) return { claimId: String(raced.id), orderId: String(raced.orderId), duplicate: true };
2957
+ throw new Error("gift issuance conflict");
2958
+ }
2959
+ }
2960
+ function buyerGiftClaims(rows4, now) {
2961
+ return rows4.map((row) => {
2962
+ const stored = String(row.status ?? "pending");
2963
+ const status = stored === "pending" && Number(row.codeExpiresAt) <= now ? "expired" : stored;
2964
+ return {
2965
+ id: String(row.id),
2966
+ offerId: String(row.offerId),
2967
+ status,
2968
+ createdAt: Number(row.createdAt),
2969
+ codeExpiresAt: Number(row.codeExpiresAt),
2970
+ ...typeof row.redeemedAt === "number" ? { redeemedAt: row.redeemedAt } : {},
2971
+ ...typeof row.revokedAt === "number" ? { revokedAt: row.revokedAt } : {}
2972
+ };
2973
+ });
2974
+ }
2975
+ async function rotateGiftClaim(db, claim, now) {
2976
+ if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) return null;
2977
+ const prepared = await prepareGiftCode(String(claim.chapterId));
2978
+ const currentRotation = Number(claim.rotationCount ?? 0);
2979
+ try {
2980
+ const write = await db.transact([{ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
2981
+ codeHash: prepared.codeHash,
2982
+ codeVersion: prepared.codeVersion,
2983
+ rotationCount: currentRotation + 1,
2984
+ codeIssuedAt: now,
2985
+ rotatedAt: now,
2986
+ attemptCount: 0,
2987
+ attemptWindowStartedAt: now
2988
+ } }], {
2989
+ mutationId: `gift-rotate:${String(claim.id)}:${currentRotation + 1}`,
2990
+ guards: [{ ns: "giftClaims", id: String(claim.id), exists: true, equals: { status: "pending", rotationCount: currentRotation } }]
2991
+ });
2992
+ return write.duplicate ? null : { code: prepared.code };
2993
+ } catch {
2994
+ return null;
2995
+ }
2996
+ }
2997
+ async function redeemGiftClaim(db, claim, beneficiary, now) {
2998
+ if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) return null;
2999
+ const chapterId = String(claim.chapterId);
3000
+ const emailNormalized = normalizeAdmissionEmail(beneficiary.email);
3001
+ const target = claim.target;
3002
+ const ops = [];
3003
+ let result;
3004
+ if (target.type === "tier") {
3005
+ const apps = (await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications;
3006
+ const existing = apps?.filter((row) => row.groupId === chapterId).sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
3007
+ const id = existing ? String(existing.id) : `gift-application:${String(claim.id)}`;
3008
+ const status = existing && existing.status !== "submitted" ? String(existing.status) : "paid_pending_vetting";
3009
+ ops.push({ t: "update", ns: "applications", id, attrs: {
3010
+ id,
3011
+ groupId: chapterId,
3012
+ email: beneficiary.email,
3013
+ emailNormalized,
3014
+ tierId: target.tierId,
3015
+ status,
3016
+ giftClaimId: String(claim.id),
3017
+ giftOrderId: String(claim.orderId),
3018
+ clerkUserId: beneficiary.userId,
3019
+ ...existing ? {} : { createdAt: now }
3020
+ } });
3021
+ result = { kind: "application", id };
3022
+ } else {
3023
+ const id = `gift-entitlement:${String(claim.id)}`;
3024
+ ops.push({ t: "update", ns: "giftEntitlements", id, attrs: {
3025
+ id,
3026
+ claimId: String(claim.id),
3027
+ orderId: String(claim.orderId),
3028
+ chapterId,
3029
+ entitlementKey: target.entitlementKey,
3030
+ beneficiaryEmailNormalized: emailNormalized,
3031
+ beneficiaryUserId: beneficiary.userId,
3032
+ active: true,
3033
+ createdAt: now
3034
+ } });
3035
+ result = { kind: "entitlement", id };
3036
+ }
3037
+ ops.unshift({ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
3038
+ status: "redeemed",
3039
+ redeemedAt: now,
3040
+ beneficiaryEmailNormalized: emailNormalized,
3041
+ beneficiaryUserId: beneficiary.userId,
3042
+ ...result.kind === "application" ? { applicationId: result.id } : { entitlementId: result.id },
3043
+ attemptCount: Number(claim.attemptCount ?? 0) + 1
3044
+ } });
3045
+ try {
3046
+ await db.transact(ops, {
3047
+ mutationId: `gift-redeem:${String(claim.id)}:${beneficiary.userId}`,
3048
+ guards: [{ ns: "giftClaims", id: String(claim.id), exists: true, equals: {
3049
+ status: "pending",
3050
+ codeVersion: Number(claim.codeVersion),
3051
+ codeHash: String(claim.codeHash)
3052
+ } }]
3053
+ });
3054
+ return result;
3055
+ } catch {
3056
+ return null;
3057
+ }
3058
+ }
3059
+ async function revokeGiftClaim(db, claim, reason, now) {
3060
+ if (claim.status === "revoked") return false;
3061
+ const ops = [{ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
3062
+ status: "revoked",
3063
+ revokedAt: now,
3064
+ revokedReason: reason
3065
+ } }];
3066
+ if (claim.entitlementId) ops.push({ t: "update", ns: "giftEntitlements", id: String(claim.entitlementId), attrs: { active: false, revokedAt: now } });
3067
+ if (claim.applicationId) ops.push({ t: "update", ns: "applications", id: String(claim.applicationId), attrs: {
3068
+ giftRevokedAt: now,
3069
+ ...reason === "refunded" ? { status: "refunded" } : { canceled: true }
3070
+ } });
3071
+ try {
3072
+ await db.transact(ops, {
3073
+ mutationId: `gift-revoke:${String(claim.id)}:${reason}`,
3074
+ guards: [{ ns: "giftClaims", id: String(claim.id), exists: true, equals: { status: String(claim.status) } }]
3075
+ });
3076
+ return true;
3077
+ } catch {
3078
+ return false;
3079
+ }
3080
+ }
3081
+
3082
+ // src/gift-checkout.ts
3083
+ var clientSecret = (value) => {
3084
+ const direct = value.client_secret;
3085
+ if (typeof direct === "string" && direct) return direct;
3086
+ const invoice = value.latest_invoice;
3087
+ const confirmation = invoice?.confirmation_secret;
3088
+ const intent = invoice?.payment_intent;
3089
+ const nested = confirmation?.client_secret ?? intent?.client_secret;
3090
+ return typeof nested === "string" && nested ? nested : null;
3091
+ };
3092
+ async function startGiftProviderCheckout(secretKey, intent, email, existingCustomerId) {
3093
+ const offer = intent.offerSnapshot;
3094
+ const metadata = {
3095
+ giftIntentId: intent.id,
3096
+ chapterId: intent.chapterId,
3097
+ environment: intent.environment,
3098
+ offerId: intent.offerId
3099
+ };
3100
+ let customerId = existingCustomerId;
3101
+ if (!customerId) {
3102
+ const customer = await stripeCall(secretKey, "POST", "/v1/customers", { email, metadata }, `gift-customer:${intent.id}`);
3103
+ if (!customer.ok) throw new Error(`gift customer create failed (${customer.status})`);
3104
+ customerId = String(customer.body.id);
3105
+ }
3106
+ if (offer.billingMode === "subscription") {
3107
+ const created2 = await stripeCall(secretKey, "POST", "/v1/subscriptions", {
3108
+ customer: customerId,
3109
+ "items[0][price]": offer.stripePriceId,
3110
+ payment_behavior: "default_incomplete",
3111
+ "payment_settings[save_default_payment_method]": "on_subscription",
3112
+ "payment_settings[payment_method_types][0]": "card",
3113
+ "expand[0]": "latest_invoice.confirmation_secret",
3114
+ metadata
3115
+ }, `gift-subscription:${intent.id}`);
3116
+ const secret2 = created2.ok ? clientSecret(created2.body) : null;
3117
+ if (!created2.ok || !secret2) throw new Error(`gift subscription create failed (${created2.status})`);
3118
+ return { clientSecret: secret2, providerObjectId: String(created2.body.id), customerId };
3119
+ }
3120
+ const price = await stripeCall(secretKey, "GET", `/v1/prices/${encodeURIComponent(offer.stripePriceId)}`);
3121
+ if (!price.ok || price.body.active === false || price.body.unit_amount !== offer.priceCents || price.body.currency !== offer.currency) {
3122
+ throw new Error("gift price does not match the signed offer");
3123
+ }
3124
+ const created = await stripeCall(secretKey, "POST", "/v1/payment_intents", {
3125
+ customer: customerId,
3126
+ amount: offer.priceCents,
3127
+ currency: offer.currency,
3128
+ "automatic_payment_methods[enabled]": true,
3129
+ metadata
3130
+ }, `gift-payment:${intent.id}`);
3131
+ const secret = created.ok ? clientSecret(created.body) : null;
3132
+ if (!created.ok || !secret) throw new Error(`gift payment create failed (${created.status})`);
3133
+ return { clientSecret: secret, providerObjectId: String(created.body.id), customerId };
3134
+ }
3135
+
3136
+ // src/gift-intents.ts
3137
+ var first2 = (value) => Array.isArray(value) ? value[0] : void 0;
3138
+ async function startGiftIntent(db, input) {
3139
+ let stored = first2((await db.query({ giftPurchaseIntents: { $: { where: { id: input.id }, limit: 1 } } })).giftPurchaseIntents);
3140
+ let giftCode;
3141
+ if (!stored) {
3142
+ const prepared = await prepareGiftCode(input.chapterId);
3143
+ const attrs = {
3144
+ id: input.id,
3145
+ chapterId: input.chapterId,
3146
+ environment: input.environment,
3147
+ purchaserType: input.purchaserType,
3148
+ purchaserEmailNormalized: input.purchaserEmailNormalized,
3149
+ ...input.purchaserApplicationId ? { purchaserApplicationId: input.purchaserApplicationId } : {},
3150
+ ...input.purchaserUserId ? { purchaserUserId: input.purchaserUserId } : {},
3151
+ offerId: input.offer.id,
3152
+ offerSnapshot: input.offer,
3153
+ status: "created",
3154
+ codeHash: prepared.codeHash,
3155
+ codeVersion: prepared.codeVersion,
3156
+ codeIssuedAt: input.now,
3157
+ createdAt: input.now,
3158
+ updatedAt: input.now
3159
+ };
3160
+ const write = await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: input.id, attrs }], {
3161
+ mutationId: `gift-intent:${input.id}`
3162
+ });
3163
+ stored = write.duplicate ? first2((await db.query({ giftPurchaseIntents: { $: { where: { id: input.id }, limit: 1 } } })).giftPurchaseIntents) : attrs;
3164
+ if (!write.duplicate) giftCode = prepared.code;
3165
+ }
3166
+ if (!stored || stored.chapterId !== input.chapterId || stored.environment !== input.environment || stored.offerId !== input.offer.id || stored.purchaserEmailNormalized !== input.purchaserEmailNormalized) {
3167
+ throw new Error("gift purchase intent does not match this checkout");
3168
+ }
3169
+ if (stored.status === "paid") throw new Error("gift purchase is already paid");
3170
+ const checkout = await startGiftProviderCheckout(
3171
+ input.stripeSecret,
3172
+ stored,
3173
+ input.purchaserEmail,
3174
+ typeof stored.providerCustomerId === "string" ? stored.providerCustomerId : input.existingCustomerId
3175
+ );
3176
+ await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: input.id, attrs: {
3177
+ status: "checkout_ready",
3178
+ providerObjectId: checkout.providerObjectId,
3179
+ ...checkout.customerId ? { providerCustomerId: checkout.customerId } : {},
3180
+ checkoutStartedAt: input.now,
3181
+ updatedAt: input.now
3182
+ } }], { mutationId: `gift-checkout-ready:${input.id}:${checkout.providerObjectId}` });
3183
+ return {
3184
+ gift: true,
3185
+ intentId: input.id,
3186
+ clientSecret: checkout.clientSecret,
3187
+ publishableKey: input.publishableKey,
3188
+ lineItems: { standardCents: input.offer.priceCents, discountCents: 0, dueTodayCents: input.offer.priceCents },
3189
+ ...giftCode ? { giftCode } : {}
3190
+ };
3191
+ }
3192
+
2248
3193
  // src/worker-routes-payments.ts
2249
3194
  var codeOf = (err) => err && typeof err === "object" && typeof err.code === "string" ? err.code : "unknown";
2250
3195
  async function firstRow2(db, ns, q) {
2251
3196
  const res = await db.query({ [ns]: { $: q } });
2252
- const rows2 = res[ns];
2253
- return Array.isArray(rows2) ? rows2[0] : void 0;
3197
+ const rows4 = res[ns];
3198
+ return Array.isArray(rows4) ? rows4[0] : void 0;
2254
3199
  }
2255
3200
  function lineItems(group) {
2256
3201
  const standard = Number(group.standardPriceCents ?? 0);
@@ -2335,6 +3280,36 @@ async function startSubscription(req, env, ctx) {
2335
3280
  const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });
2336
3281
  const secretKey = await getVaultSecret(db, "stripe_secret_key");
2337
3282
  if (!group || !secretKey) return json({ error: "payments not configured" }, 503);
3283
+ if (typeof app.giftOfferId === "string") {
3284
+ const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3285
+ const offer = findGiftOffer(control?.document.giftOffers, app.giftOfferId, Date.now());
3286
+ if (!offer || typeof group.stripePublishableKey !== "string" || !group.stripePublishableKey) {
3287
+ return json({ error: "gift offer is unavailable" }, 409);
3288
+ }
3289
+ try {
3290
+ const started = await startGiftIntent(db, {
3291
+ id: typeof app.giftIntentId === "string" ? app.giftIntentId : `gift-intent:${applicationId}`,
3292
+ chapterId: ctx.chapter.id,
3293
+ environment: env.ODLA_ENV,
3294
+ offer,
3295
+ purchaserType: "application",
3296
+ purchaserEmail: String(app.email ?? ""),
3297
+ purchaserEmailNormalized: normalizeAdmissionEmail(String(app.email ?? "")),
3298
+ purchaserApplicationId: applicationId,
3299
+ stripeSecret: secretKey,
3300
+ publishableKey: group.stripePublishableKey,
3301
+ now: Date.now()
3302
+ });
3303
+ await db.transact([{ t: "update", ns: "applications", id: applicationId, attrs: {
3304
+ giftIntentId: started.intentId,
3305
+ giftCheckoutStartedAt: Date.now(),
3306
+ refundPolicyAckAt: Date.now()
3307
+ } }]);
3308
+ return json(started);
3309
+ } catch (error) {
3310
+ return json({ error: error instanceof Error ? error.message : "gift checkout failed" }, 502);
3311
+ }
3312
+ }
2338
3313
  const tier = findTier(
2339
3314
  await loadTiers(db, group),
2340
3315
  typeof app.tierId === "string" ? app.tierId : null
@@ -2376,17 +3351,54 @@ async function ingestWebhook(req, env, ctx) {
2376
3351
  if (!ingest.ok) return json({ error: "invalid signature" }, 400);
2377
3352
  const { eventId, event } = ingest;
2378
3353
  if (event.kind === "ignored") return json({ ok: true, ignored: event.type });
3354
+ if (event.kind === "gift_paid" || event.kind === "gift_revoked") {
3355
+ const intent = await firstRow2(db, "giftPurchaseIntents", { where: { id: event.giftIntentId }, limit: 1 });
3356
+ if (!intent || intent.chapterId !== ctx.chapter.id || intent.environment !== env.ODLA_ENV) {
3357
+ return json({ ok: true, matched: false });
3358
+ }
3359
+ if (event.kind === "gift_paid") {
3360
+ if (intent.providerObjectId !== event.providerObjectId) return json({ ok: true, matched: false });
3361
+ if (intent.status === "refunded" || intent.status === "canceled") return json({ ok: true, matched: false });
3362
+ try {
3363
+ const issued = await issueGiftClaim(db, intent, {
3364
+ eventId,
3365
+ objectId: event.providerObjectId
3366
+ }, Date.now());
3367
+ return json({ ok: true, matched: true, claimId: issued.claimId });
3368
+ } catch {
3369
+ return json({ error: "gift issuance failed" }, 503);
3370
+ }
3371
+ }
3372
+ await db.transact([{ t: "update", ns: "giftPurchaseIntents", id: event.giftIntentId, attrs: {
3373
+ status: event.reason,
3374
+ updatedAt: Date.now()
3375
+ } }], { mutationId: `gift-revoked-intent:${eventId}` });
3376
+ const claim = await firstRow2(db, "giftClaims", { where: { intentId: event.giftIntentId }, limit: 1 });
3377
+ if (claim) await revokeGiftClaim(db, claim, event.reason, Date.now());
3378
+ return json({ ok: true, matched: Boolean(claim) });
3379
+ }
2379
3380
  const app = await findApplication(db, event);
2380
3381
  if (!app) return json({ ok: true, matched: false });
2381
3382
  const patch = webhookPatch(event, String(app.status ?? ""));
3383
+ let next = app;
2382
3384
  if (Object.keys(patch).length) {
2383
3385
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
2384
- const next = { ...app, ...patch };
3386
+ next = { ...app, ...patch };
2385
3387
  await syncApplicationToCrm(crmDeps(db, ctx), { app: next, stage: String(next.status ?? "") }).catch(() => void 0);
2386
3388
  }
2387
3389
  if (event.kind === "first_payment") {
2388
3390
  await notifyPaymentConfirmed(db, env, eventId, app);
2389
3391
  if (ctx.chapter.sends.adminNotification === "payment") await notifyAdminOfPayment(db, env, eventId, app);
3392
+ if (typeof next.interviewWaivedAt === "number") {
3393
+ await approveApplication({
3394
+ db,
3395
+ env,
3396
+ ctx,
3397
+ app: next,
3398
+ origin: new URL(req.url).origin,
3399
+ reason: "interview_waiver"
3400
+ });
3401
+ }
2390
3402
  }
2391
3403
  return json({ ok: true });
2392
3404
  }
@@ -2397,6 +3409,144 @@ var handlePayments = async (req, url, env, ctx) => {
2397
3409
  return null;
2398
3410
  };
2399
3411
 
3412
+ // src/worker-routes-gifts.ts
3413
+ var first3 = (value) => Array.isArray(value) ? value[0] : void 0;
3414
+ var rows2 = (value) => Array.isArray(value) ? value : [];
3415
+ var invalid = () => json({ error: "gift code is invalid or unavailable" }, 400);
3416
+ var userFor = async (req, env, ctx) => {
3417
+ const user = await ctx.verifyUser(req, env);
3418
+ return user?.email ? user : null;
3419
+ };
3420
+ var loadClaim = async (db, id) => first3((await db.query({ giftClaims: { $: { where: { id }, limit: 1 } } })).giftClaims);
3421
+ async function consumeRateLimit(db, chapterId, requester, now) {
3422
+ const bucket = await giftRequesterBucket(chapterId, requester);
3423
+ const id = `gift-rate:${chapterId}:${bucket}`;
3424
+ const current = first3((await db.query({ giftRedemptionAttempts: { $: { where: { id }, limit: 1 } } })).giftRedemptionAttempts);
3425
+ const within = Boolean(current && now - Number(current.windowStartedAt) < 15 * 6e4);
3426
+ const attemptCount = within ? Number(current?.attemptCount ?? 0) + 1 : 1;
3427
+ if (attemptCount > 10) return false;
3428
+ await db.transact([{ t: "update", ns: "giftRedemptionAttempts", id, attrs: {
3429
+ id,
3430
+ chapterId,
3431
+ bucket,
3432
+ attemptCount,
3433
+ windowStartedAt: within ? Number(current?.windowStartedAt) : now,
3434
+ updatedAt: now
3435
+ } }]);
3436
+ return true;
3437
+ }
3438
+ var handleGifts = async (req, url, env, ctx) => {
3439
+ if (ctx.chapter.mode !== "chapter" || !url.pathname.startsWith("/api/gifts")) return null;
3440
+ const db = ctx.makeDb(env);
3441
+ if (req.method === "GET" && url.pathname === "/api/gifts") {
3442
+ const user = await userFor(req, env, ctx);
3443
+ if (!user) return json({ error: "unauthorized" }, 401);
3444
+ const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3445
+ const found = await db.query({ giftClaims: { $: { where: {
3446
+ purchaserEmailNormalized: normalizeAdmissionEmail(user.email),
3447
+ chapterId: ctx.chapter.id
3448
+ }, limit: 200 } } });
3449
+ const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3450
+ return json({
3451
+ offers: publicGiftOffers(control?.document.giftOffers, Date.now()),
3452
+ claims: buyerGiftClaims(rows2(found.giftClaims).sort((a, b) => Number(b.createdAt) - Number(a.createdAt)), Date.now()),
3453
+ refundPolicyText: typeof group?.refundPolicyText === "string" ? group.refundPolicyText : ""
3454
+ });
3455
+ }
3456
+ if (req.method === "POST" && url.pathname === "/api/gifts/checkout") {
3457
+ const user = await userFor(req, env, ctx);
3458
+ if (!user) return json({ error: "unauthorized" }, 401);
3459
+ let body;
3460
+ try {
3461
+ body = JSON.parse(await req.text());
3462
+ } catch {
3463
+ return json({ error: "invalid JSON body" }, 400);
3464
+ }
3465
+ const purchaseId = typeof body.purchaseId === "string" ? body.purchaseId : "";
3466
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(purchaseId)) return json({ error: "purchaseId is invalid" }, 400);
3467
+ const control = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
3468
+ const offer = findGiftOffer(control?.document.giftOffers, typeof body.offerId === "string" ? body.offerId : "", Date.now());
3469
+ if (!offer) return json({ error: "gift offer is unavailable" }, 409);
3470
+ const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
3471
+ const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
3472
+ if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
3473
+ const emailNormalized = normalizeAdmissionEmail(user.email);
3474
+ const applications = rows2((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
3475
+ const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
3476
+ try {
3477
+ return json(await startGiftIntent(db, {
3478
+ id: `gift-intent:${ctx.chapter.id}:${purchaseId}`,
3479
+ chapterId: ctx.chapter.id,
3480
+ environment: env.ODLA_ENV,
3481
+ offer,
3482
+ purchaserType: "member",
3483
+ purchaserEmail: user.email,
3484
+ purchaserEmailNormalized: emailNormalized,
3485
+ purchaserUserId: user.userId,
3486
+ ...typeof app?.stripeCustomerId === "string" ? { existingCustomerId: app.stripeCustomerId } : {},
3487
+ stripeSecret,
3488
+ publishableKey: group.stripePublishableKey,
3489
+ now: Date.now()
3490
+ }));
3491
+ } catch (error) {
3492
+ return json({ error: error instanceof Error ? error.message : "gift checkout failed" }, 502);
3493
+ }
3494
+ }
3495
+ const rotateMatch = url.pathname.match(/^\/api\/gifts\/([^/]+)\/rotate$/);
3496
+ if (req.method === "POST" && rotateMatch) {
3497
+ let claimId = "";
3498
+ try {
3499
+ claimId = decodeURIComponent(rotateMatch[1]);
3500
+ } catch {
3501
+ return invalid();
3502
+ }
3503
+ if (!claimId.startsWith("gift-claim:")) return invalid();
3504
+ const claim = await loadClaim(db, claimId);
3505
+ if (!claim || claim.chapterId !== ctx.chapter.id || Number(claim.rotationCount ?? 0) >= 10) return invalid();
3506
+ const user = await userFor(req, env, ctx);
3507
+ let applicationId = "";
3508
+ if (!user) {
3509
+ try {
3510
+ applicationId = String(JSON.parse(await req.text()).applicationId ?? "");
3511
+ } catch {
3512
+ return invalid();
3513
+ }
3514
+ }
3515
+ const owns = user ? claim.purchaserEmailNormalized === normalizeAdmissionEmail(user.email) : claim.purchaserApplicationId === applicationId;
3516
+ if (!owns) return invalid();
3517
+ const rotated = await rotateGiftClaim(db, claim, Date.now());
3518
+ return rotated ? json(rotated) : invalid();
3519
+ }
3520
+ if (req.method === "POST" && url.pathname === "/api/gifts/redeem") {
3521
+ const user = await userFor(req, env, ctx);
3522
+ if (!user) return json({ error: "unauthorized" }, 401);
3523
+ const now = Date.now();
3524
+ if (!await consumeRateLimit(db, ctx.chapter.id, `${user.userId}:${req.headers.get("cf-connecting-ip") ?? "no-ip"}`, now)) {
3525
+ return json({ error: "too many attempts" }, 429);
3526
+ }
3527
+ let code = "";
3528
+ try {
3529
+ code = String(JSON.parse(await req.text()).code ?? "").trim();
3530
+ } catch {
3531
+ return invalid();
3532
+ }
3533
+ if (!/^gift1_[A-Za-z0-9_-]{43}$/.test(code)) return invalid();
3534
+ const codeHash = await hashGiftCode(ctx.chapter.id, code);
3535
+ const claim = first3((await db.query({ giftClaims: { $: { where: { codeHash }, limit: 1 } } })).giftClaims);
3536
+ if (!claim || claim.chapterId !== ctx.chapter.id) return invalid();
3537
+ if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) {
3538
+ await db.transact([{ t: "update", ns: "giftClaims", id: String(claim.id), attrs: {
3539
+ attemptCount: Number(claim.attemptCount ?? 0) + 1,
3540
+ ...claim.status === "pending" ? { status: "expired" } : {}
3541
+ } }]).catch(() => void 0);
3542
+ return invalid();
3543
+ }
3544
+ const redeemed = await redeemGiftClaim(db, claim, { email: user.email, userId: user.userId }, now);
3545
+ return redeemed ? json({ ok: true, ...redeemed }) : invalid();
3546
+ }
3547
+ return null;
3548
+ };
3549
+
2400
3550
  // src/worker-routes-admin.ts
2401
3551
  var import_calendar2 = require("@odla-ai/calendar");
2402
3552
 
@@ -2468,8 +3618,8 @@ async function upcomingEvents(env) {
2468
3618
  const res = await cal.availability.upcoming();
2469
3619
  return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
2470
3620
  }
2471
- function toReconcile(rows2) {
2472
- return rows2.map((m) => ({
3621
+ function toReconcile(rows4) {
3622
+ return rows4.map((m) => ({
2473
3623
  id: String(m.id),
2474
3624
  applicationId: String(m.applicationId),
2475
3625
  googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
@@ -2491,12 +3641,12 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
2491
3641
  const query = await db.query({
2492
3642
  meetings: { $: { where: all ? {} : { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } }
2493
3643
  });
2494
- let rows2 = Array.isArray(query.meetings) ? query.meetings : [];
2495
- if (Number.isFinite(from)) rows2 = rows2.filter((m) => Number(m.startAt ?? 0) >= from);
2496
- if (Number.isFinite(to)) rows2 = rows2.filter((m) => Number(m.startAt ?? 0) <= to);
3644
+ let rows4 = Array.isArray(query.meetings) ? query.meetings : [];
3645
+ if (Number.isFinite(from)) rows4 = rows4.filter((m) => Number(m.startAt ?? 0) >= from);
3646
+ if (Number.isFinite(to)) rows4 = rows4.filter((m) => Number(m.startAt ?? 0) <= to);
2497
3647
  let decisions = [];
2498
3648
  try {
2499
- decisions = reconcileMeetings(toReconcile(rows2), await upcomingEvents(env), Date.now());
3649
+ decisions = reconcileMeetings(toReconcile(rows4), await upcomingEvents(env), Date.now());
2500
3650
  } catch {
2501
3651
  }
2502
3652
  const patched = /* @__PURE__ */ new Map();
@@ -2523,69 +3673,10 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
2523
3673
  };
2524
3674
  const group = (await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups?.[0];
2525
3675
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
2526
- const meetings = rows2.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
3676
+ const meetings = rows4.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {}, applicant: applicantOf(m) })).filter((m) => all || m.status === "scheduled");
2527
3677
  return json({ meetings, adopted: decisions.length, timezone });
2528
3678
  };
2529
3679
 
2530
- // src/clerk-roles.ts
2531
- var CLERK_API = "https://api.clerk.com";
2532
- var DEFAULT_ROLE = "provisional";
2533
- var PAGE = 100;
2534
- function toRecord(u) {
2535
- if (typeof u.id !== "string") return null;
2536
- const pm = u.public_metadata ?? {};
2537
- const privateMetadata = u.private_metadata ?? {};
2538
- const rawProfile = privateMetadata.profile;
2539
- const profile = rawProfile && typeof rawProfile === "object" && !Array.isArray(rawProfile) ? rawProfile : {};
2540
- const applicationId = typeof privateMetadata.applicationId === "string" ? privateMetadata.applicationId : void 0;
2541
- const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
2542
- const email = u.email_addresses?.[0]?.email_address;
2543
- return {
2544
- id: u.id,
2545
- email: typeof email === "string" ? email : void 0,
2546
- role,
2547
- publicMetadata: pm,
2548
- privateMetadata,
2549
- profile,
2550
- ...applicationId ? { applicationId } : {}
2551
- };
2552
- }
2553
- async function clerkGet(path, secretKey, fetchImpl) {
2554
- const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
2555
- if (!res.ok) throw new Error(`clerk GET ${path} \u2192 ${res.status}`);
2556
- return res.json();
2557
- }
2558
- async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
2559
- const data = await clerkGet(`/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, secretKey, fetchImpl).catch(() => null);
2560
- const user = Array.isArray(data) ? data[0] : void 0;
2561
- return user ? toRecord(user) : null;
2562
- }
2563
- async function clerkGetUser(secretKey, id, fetchImpl = fetch) {
2564
- const data = await clerkGet(`/v1/users/${encodeURIComponent(id)}`, secretKey, fetchImpl).catch(() => null);
2565
- return data ? toRecord(data) : null;
2566
- }
2567
- async function clerkListUsers(secretKey, fetchImpl = fetch) {
2568
- const out = [];
2569
- for (let offset = 0; ; offset += PAGE) {
2570
- const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
2571
- const page = Array.isArray(data) ? data : [];
2572
- for (const u of page) {
2573
- const record = toRecord(u);
2574
- if (record) out.push(record);
2575
- }
2576
- if (page.length < PAGE) break;
2577
- }
2578
- return out;
2579
- }
2580
- async function clerkSetRole(secretKey, id, role, fetchImpl = fetch) {
2581
- const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id)}/metadata`, {
2582
- method: "PATCH",
2583
- headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
2584
- body: JSON.stringify({ public_metadata: { role } })
2585
- });
2586
- return res.ok;
2587
- }
2588
-
2589
3680
  // src/worker-routes-admin-people.ts
2590
3681
  async function adminGate(req, env, ctx) {
2591
3682
  const rawDb = ctx.makeDb(env);
@@ -2603,16 +3694,16 @@ var crmDeps2 = (db, ctx) => ({
2603
3694
  });
2604
3695
  var handleAdminCrmSync = async (req, url, env, ctx) => {
2605
3696
  if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
2606
- const gate8 = await adminGate(req, env, ctx);
2607
- if (gate8 instanceof Response) return gate8;
2608
- const result = await backfillCrm(crmDeps2(gate8.db, ctx));
3697
+ const gate9 = await adminGate(req, env, ctx);
3698
+ if (gate9 instanceof Response) return gate9;
3699
+ const result = await backfillCrm(crmDeps2(gate9.db, ctx));
2609
3700
  return json({ ok: true, ...result });
2610
3701
  };
2611
3702
  var handleAdminPeople = async (req, url, env, ctx) => {
2612
3703
  if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
2613
- const gate8 = await adminGate(req, env, ctx);
2614
- if (gate8 instanceof Response) return gate8;
2615
- const { db } = gate8;
3704
+ const gate9 = await adminGate(req, env, ctx);
3705
+ if (gate9 instanceof Response) return gate9;
3706
+ const { db } = gate9;
2616
3707
  const sk = await getVaultSecret(db, "clerk_secret_key");
2617
3708
  const [appsRes, usersRes, roleList] = await Promise.all([
2618
3709
  db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
@@ -2656,16 +3747,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
2656
3747
  });
2657
3748
  }
2658
3749
  }
2659
- const rows2 = [...people.values()].sort(
3750
+ const rows4 = [...people.values()].sort(
2660
3751
  (x, y) => (y.application?.createdAt ?? -1) - (x.application?.createdAt ?? -1)
2661
3752
  );
2662
- return json({ people: rows2 });
3753
+ return json({ people: rows4 });
2663
3754
  };
2664
3755
  var handleAdminPeopleAccess = async (req, url, env, ctx) => {
2665
3756
  if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
2666
- const gate8 = await adminGate(req, env, ctx);
2667
- if (gate8 instanceof Response) return gate8;
2668
- const { db } = gate8;
3757
+ const gate9 = await adminGate(req, env, ctx);
3758
+ if (gate9 instanceof Response) return gate9;
3759
+ const { db } = gate9;
2669
3760
  const targetId = url.searchParams.get("userId") ?? "";
2670
3761
  if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
2671
3762
  const sk = await getVaultSecret(db, "clerk_secret_key");
@@ -2676,9 +3767,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
2676
3767
  };
2677
3768
  var handleAdminPeopleRole = async (req, url, env, ctx) => {
2678
3769
  if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
2679
- const gate8 = await adminGate(req, env, ctx);
2680
- if (gate8 instanceof Response) return gate8;
2681
- const { db, actor } = gate8;
3770
+ const gate9 = await adminGate(req, env, ctx);
3771
+ if (gate9 instanceof Response) return gate9;
3772
+ const { db, actor } = gate9;
2682
3773
  let body;
2683
3774
  try {
2684
3775
  body = await req.json();
@@ -2825,17 +3916,17 @@ function subAnnualCents(sub) {
2825
3916
  }
2826
3917
 
2827
3918
  // src/worker-routes-admin-dashboard.ts
2828
- async function gate(req, env, ctx) {
3919
+ async function gate2(req, env, ctx) {
2829
3920
  const rawDb = ctx.makeDb(env);
2830
3921
  const u = await ctx.verifyUser(req, env);
2831
3922
  if (!u) return json({ error: "unauthorized" }, 401);
2832
3923
  if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
2833
3924
  return rawDb;
2834
3925
  }
2835
- var rows = (v) => Array.isArray(v) ? v : [];
3926
+ var rows3 = (v) => Array.isArray(v) ? v : [];
2836
3927
  var handleAdminDashboard = async (req, url, env, ctx) => {
2837
3928
  if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
2838
- const got = await gate(req, env, ctx);
3929
+ const got = await gate2(req, env, ctx);
2839
3930
  if (got instanceof Response) return got;
2840
3931
  const db = got;
2841
3932
  const now = Date.now();
@@ -2847,7 +3938,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
2847
3938
  db.query({ crm_record: { $: { where: { type: "person" }, limit: 1e3 } } }),
2848
3939
  db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })
2849
3940
  ]);
2850
- const apps = rows(appsRes.applications);
3941
+ const apps = rows3(appsRes.applications);
2851
3942
  const applications = {
2852
3943
  total: apps.length,
2853
3944
  last7: apps.filter((a) => a.createdAt >= d7).length,
@@ -2859,7 +3950,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
2859
3950
  pipeline[s] = 0;
2860
3951
  pipelineDelta[s] = 0;
2861
3952
  }
2862
- for (const r of rows(recsRes.crm_record)) {
3953
+ for (const r of rows3(recsRes.crm_record)) {
2863
3954
  const s = r.stage;
2864
3955
  if (s in pipeline) {
2865
3956
  pipeline[s] = (pipeline[s] ?? 0) + 1;
@@ -2867,7 +3958,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
2867
3958
  if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
2868
3959
  }
2869
3960
  }
2870
- const meetings = rows(meetingsRes.meetings);
3961
+ const meetings = rows3(meetingsRes.meetings);
2871
3962
  const appById = new Map(apps.map((a) => [a.id, a]));
2872
3963
  const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
2873
3964
  const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
@@ -2892,7 +3983,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
2892
3983
  if (sk) {
2893
3984
  const subsRes = await stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" });
2894
3985
  if (subsRes.ok) {
2895
- const subs = rows(subsRes.body.data);
3986
+ const subs = rows3(subsRes.body.data);
2896
3987
  const subMs = (s) => (s.created ?? 0) * 1e3;
2897
3988
  membersSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: 1 })), now);
2898
3989
  revenueSeries = dashboardMetricData(subs.map((s) => ({ t: subMs(s), v: subAnnualCents(s) })), now);
@@ -2912,7 +4003,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
2912
4003
  };
2913
4004
  var handleAdminBilling = async (req, url, env, ctx) => {
2914
4005
  if (req.method !== "GET" || url.pathname !== "/api/admin/billing") return null;
2915
- const got = await gate(req, env, ctx);
4006
+ const got = await gate2(req, env, ctx);
2916
4007
  if (got instanceof Response) return got;
2917
4008
  const db = got;
2918
4009
  const sk = await getVaultSecret(db, "stripe_secret_key");
@@ -2924,9 +4015,9 @@ var handleAdminBilling = async (req, url, env, ctx) => {
2924
4015
  ]);
2925
4016
  const testMode = String(groupRes.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
2926
4017
  if (!subsRes.ok) return json({ error: "billing lookup failed upstream" }, 502);
2927
- const subById = new Map(rows(subsRes.body.data).map((s) => [s.id, s]));
4018
+ const subById = new Map(rows3(subsRes.body.data).map((s) => [s.id, s]));
2928
4019
  const billingRows = [];
2929
- for (const a of rows(appsRes.applications)) {
4020
+ for (const a of rows3(appsRes.applications)) {
2930
4021
  if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
2931
4022
  const sub = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
2932
4023
  const items = sub?.items?.data ?? [];
@@ -2970,7 +4061,7 @@ var handleAdminBilling = async (req, url, env, ctx) => {
2970
4061
 
2971
4062
  // src/worker-routes-admin-lifecycle.ts
2972
4063
  var import_calendar3 = require("@odla-ai/calendar");
2973
- async function gate2(req, env, ctx) {
4064
+ async function gate3(req, env, ctx) {
2974
4065
  const rawDb = ctx.makeDb(env);
2975
4066
  const u = await ctx.verifyUser(req, env);
2976
4067
  if (!u) return json({ error: "unauthorized" }, 401);
@@ -2996,7 +4087,7 @@ async function loadGroup(db, id) {
2996
4087
  var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
2997
4088
  const m = url.pathname.match(/^\/api\/admin\/meetings\/([0-9a-f-]+)\/reschedule$/);
2998
4089
  if (req.method !== "POST" || !m) return null;
2999
- const got = await gate2(req, env, ctx);
4090
+ const got = await gate3(req, env, ctx);
3000
4091
  if (got instanceof Response) return got;
3001
4092
  const db = got;
3002
4093
  const body = await readJson(req);
@@ -3037,7 +4128,7 @@ var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
3037
4128
  var handleAdminMeetingCancel = async (req, url, env, ctx) => {
3038
4129
  const m = url.pathname.match(/^\/api\/admin\/meetings\/([0-9a-f-]+)\/cancel$/);
3039
4130
  if (req.method !== "POST" || !m) return null;
3040
- const got = await gate2(req, env, ctx);
4131
+ const got = await gate3(req, env, ctx);
3041
4132
  if (got instanceof Response) return got;
3042
4133
  const db = got;
3043
4134
  const { meetings } = await db.query({ meetings: { $: { where: { id: m[1] }, limit: 1 } } });
@@ -3060,43 +4151,20 @@ var handleAdminMeetingCancel = async (req, url, env, ctx) => {
3060
4151
  var handleAdminApprove = async (req, url, env, ctx) => {
3061
4152
  const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/approve$/);
3062
4153
  if (req.method !== "POST" || !m) return null;
3063
- const got = await gate2(req, env, ctx);
4154
+ const got = await gate3(req, env, ctx);
3064
4155
  if (got instanceof Response) return got;
3065
4156
  const db = got;
3066
4157
  const id = m[1];
3067
4158
  const app = await loadApp(db, id);
3068
4159
  if (!app) return json({ error: "not found" }, 404);
3069
- if (!canApprove(String(app.status), ctx.chapter.pipeline)) return json({ error: `cannot approve from status "${String(app.status)}"` }, 409);
3070
- const target = "approved";
3071
- await db.transact([{ t: "update", ns: "applications", id, attrs: { status: target } }]);
3072
- await syncApplicationToCrm(crmDeps(db, ctx), { app: { ...app, status: target }, stage: target }).catch(() => void 0);
3073
- const { promoteTo, send } = ctx.chapter.operations.onApprove;
3074
- let rolePromoted = false;
3075
- const sk = await getVaultSecret(db, "clerk_secret_key");
3076
- if (promoteTo !== false && sk) {
3077
- let userId = app.clerkUserId || null;
3078
- if (!userId) {
3079
- const found = await clerkGetUserByEmail(sk, String(app.email));
3080
- userId = found?.id ?? null;
3081
- if (userId) await db.transact([{ t: "update", ns: "applications", id, attrs: { clerkUserId: userId } }]);
3082
- }
3083
- if (userId) rolePromoted = await clerkSetRole(sk, userId, promoteTo);
3084
- }
3085
- let emailLogged = false;
3086
- const group = await loadGroup(db, String(app.groupId ?? ctx.chapter.id));
3087
- if (send !== false && group) {
3088
- const res = await sendTemplated(
3089
- { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
3090
- { group: emailGroupFrom(group), template: send, to: String(app.email), vars: { firstName: String(app.firstName ?? ""), membersUrl: `${url.origin}/members/` }, applicationId: id, dedupeKey: `approve:${id}` }
3091
- );
3092
- emailLogged = res.sent;
3093
- }
3094
- return json({ ok: true, status: target, rolePromoted, emailLogged });
4160
+ const result = await approveApplication({ db, env, ctx, app, origin: url.origin, reason: "manual" });
4161
+ if (!result.ok) return json({ error: result.error }, result.status);
4162
+ return json({ ok: true, status: "approved", rolePromoted: result.rolePromoted, emailLogged: result.emailLogged });
3095
4163
  };
3096
4164
  var handleAdminRefund = async (req, url, env, ctx) => {
3097
4165
  const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/refund$/);
3098
4166
  if (req.method !== "POST" || !m) return null;
3099
- const got = await gate2(req, env, ctx);
4167
+ const got = await gate3(req, env, ctx);
3100
4168
  if (got instanceof Response) return got;
3101
4169
  const db = got;
3102
4170
  const app = await loadApp(db, m[1]);
@@ -3128,7 +4196,7 @@ var handleAdminRefund = async (req, url, env, ctx) => {
3128
4196
  var handleAdminApplicationPatch = async (req, url, env, ctx) => {
3129
4197
  const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)$/);
3130
4198
  if (req.method !== "PATCH" || !m) return null;
3131
- const got = await gate2(req, env, ctx);
4199
+ const got = await gate3(req, env, ctx);
3132
4200
  if (got instanceof Response) return got;
3133
4201
  const db = got;
3134
4202
  const id = m[1];
@@ -3156,7 +4224,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
3156
4224
  };
3157
4225
 
3158
4226
  // src/worker-routes-admin-comms.ts
3159
- async function gate3(req, env, ctx) {
4227
+ async function gate4(req, env, ctx) {
3160
4228
  const rawDb = ctx.makeDb(env);
3161
4229
  const u = await ctx.verifyUser(req, env);
3162
4230
  if (!u) return json({ error: "unauthorized" }, 401);
@@ -3178,7 +4246,7 @@ var notifyDeps = (db, env) => ({
3178
4246
  });
3179
4247
  var handleAdminGroupEmail = async (req, url, env, ctx) => {
3180
4248
  if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
3181
- const got = await gate3(req, env, ctx);
4249
+ const got = await gate4(req, env, ctx);
3182
4250
  if (got instanceof Response) return got;
3183
4251
  const db = got;
3184
4252
  const group = await loadGroup2(db, ctx.chapter.id);
@@ -3219,11 +4287,11 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
3219
4287
  for (const key of EMAIL_TEMPLATE_NAMES) {
3220
4288
  const t = templates[key];
3221
4289
  const subject = typeof t?.subject === "string" ? t.subject.trim() : "";
3222
- const text2 = typeof t?.text === "string" ? t.text : "";
3223
- if (!subject || !text2.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
4290
+ const text3 = typeof t?.text === "string" ? t.text : "";
4291
+ if (!subject || !text3.trim()) return json({ error: `template "${key}" needs a subject and a body` }, 400);
3224
4292
  if (/[\r\n]/.test(subject) || subject.length > 200) return json({ error: `template "${key}" subject must be a single line under 200 characters` }, 400);
3225
- if (text2.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
3226
- clean[key] = { subject, text: text2, enabled: t?.enabled !== false };
4293
+ if (text3.length > 1e4) return json({ error: `template "${key}" body is too long` }, 400);
4294
+ clean[key] = { subject, text: text3, enabled: t?.enabled !== false };
3227
4295
  }
3228
4296
  const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3229
4297
  const notificationEmail = str2(body.notificationEmail).trim();
@@ -3240,7 +4308,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
3240
4308
  };
3241
4309
  var handleAdminEmailLog = async (req, url, env, ctx) => {
3242
4310
  if (req.method !== "GET" || url.pathname !== "/api/admin/email/log") return null;
3243
- const got = await gate3(req, env, ctx);
4311
+ const got = await gate4(req, env, ctx);
3244
4312
  if (got instanceof Response) return got;
3245
4313
  const { emailLog } = await got.query({ emailLog: { $: { order: { sentAt: "desc" }, limit: 50 } } });
3246
4314
  const sends = (emailLog ?? []).map((r) => ({
@@ -3257,7 +4325,7 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
3257
4325
  };
3258
4326
  var handleAdminEmailTest = async (req, url, env, ctx) => {
3259
4327
  if (req.method !== "POST" || url.pathname !== "/api/admin/email/test") return null;
3260
- const got = await gate3(req, env, ctx);
4328
+ const got = await gate4(req, env, ctx);
3261
4329
  if (got instanceof Response) return got;
3262
4330
  const db = got;
3263
4331
  let body;
@@ -3285,7 +4353,7 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
3285
4353
  var handleAdminComms = async (req, url, env, ctx) => {
3286
4354
  const m = url.pathname.match(/^\/api\/admin\/people\/([0-9a-fA-F-]+)\/comms$/);
3287
4355
  if (req.method !== "GET" || !m) return null;
3288
- const got = await gate3(req, env, ctx);
4356
+ const got = await gate4(req, env, ctx);
3289
4357
  if (got instanceof Response) return got;
3290
4358
  const db = got;
3291
4359
  const appId = m[1];
@@ -3318,7 +4386,7 @@ var handleAdminComms = async (req, url, env, ctx) => {
3318
4386
 
3319
4387
  // src/worker-routes-network.ts
3320
4388
  var import_crm6 = require("@odla-ai/crm");
3321
- var import_db5 = require("@odla-ai/db");
4389
+ var import_db6 = require("@odla-ai/db");
3322
4390
 
3323
4391
  // src/worker-network-fetch.ts
3324
4392
  function targetFetcher(env, target) {
@@ -3336,8 +4404,8 @@ function fetchNetworkTarget(env, target, destination, init) {
3336
4404
 
3337
4405
  // src/worker-network-push.ts
3338
4406
  var import_crm5 = require("@odla-ai/crm");
3339
- var import_db4 = require("@odla-ai/db");
3340
- async function pushNetworkRecord(db, env, ctx, target, record) {
4407
+ var import_db5 = require("@odla-ai/db");
4408
+ async function pushNetworkRecord(db, env, ctx, target, record2) {
3341
4409
  const secret = await getVaultSecret(db, target.secretName);
3342
4410
  const crmDeps3 = {
3343
4411
  crm: ctx.chapter.crm,
@@ -3348,7 +4416,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
3348
4416
  if (!secret) {
3349
4417
  const error = `vault secret "${target.secretName}" is missing`;
3350
4418
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
3351
- recordId: record.id,
4419
+ recordId: record2.id,
3352
4420
  targetId: target.id,
3353
4421
  status: "failed",
3354
4422
  payloadVersion: 2,
@@ -3358,7 +4426,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
3358
4426
  }
3359
4427
  let payload;
3360
4428
  try {
3361
- payload = sharedRecordFromCrm(ctx.chapter.crm, record, target, ctx.chapter.id);
4429
+ payload = sharedRecordFromCrm(ctx.chapter.crm, record2, target, ctx.chapter.id);
3362
4430
  } catch (err) {
3363
4431
  return {
3364
4432
  id: target.id,
@@ -3370,7 +4438,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
3370
4438
  const payloadBody = JSON.stringify(payload);
3371
4439
  const destination = new URL("/api/network/shared", target.url);
3372
4440
  try {
3373
- const signed = await (0, import_db4.signFederatedRequest)({
4441
+ const signed = await (0, import_db5.signFederatedRequest)({
3374
4442
  secret,
3375
4443
  sender: ctx.chapter.id,
3376
4444
  method: "POST",
@@ -3387,7 +4455,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
3387
4455
  if (!res.ok) {
3388
4456
  const error = responseBody.error ?? "follower rejected the record";
3389
4457
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
3390
- recordId: record.id,
4458
+ recordId: record2.id,
3391
4459
  targetId: target.id,
3392
4460
  status: "failed",
3393
4461
  payloadVersion: 2,
@@ -3402,7 +4470,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
3402
4470
  };
3403
4471
  }
3404
4472
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
3405
- recordId: record.id,
4473
+ recordId: record2.id,
3406
4474
  targetId: target.id,
3407
4475
  status: "delivered",
3408
4476
  payloadVersion: 2,
@@ -3418,7 +4486,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
3418
4486
  } catch (err) {
3419
4487
  const error = err instanceof Error ? err.message : "delivery failed";
3420
4488
  await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
3421
- recordId: record.id,
4489
+ recordId: record2.id,
3422
4490
  targetId: target.id,
3423
4491
  status: "failed",
3424
4492
  payloadVersion: 2,
@@ -3429,7 +4497,7 @@ async function pushNetworkRecord(db, env, ctx, target, record) {
3429
4497
  }
3430
4498
 
3431
4499
  // src/worker-routes-network.ts
3432
- async function gate4(req, env, ctx) {
4500
+ async function gate5(req, env, ctx) {
3433
4501
  const db = ctx.makeDb(env);
3434
4502
  const user = await ctx.verifyUser(req, env);
3435
4503
  if (!user) return { response: json({ error: "unauthorized" }, 401) };
@@ -3438,7 +4506,7 @@ async function gate4(req, env, ctx) {
3438
4506
  }
3439
4507
  var handleAdminNetworkTargets = async (req, url, env, ctx) => {
3440
4508
  if (req.method !== "GET" || url.pathname !== "/api/admin/network/targets") return null;
3441
- const got = await gate4(req, env, ctx);
4509
+ const got = await gate5(req, env, ctx);
3442
4510
  if ("response" in got) return got.response;
3443
4511
  return json({
3444
4512
  targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields, sharedNotes }) => ({
@@ -3455,7 +4523,7 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
3455
4523
  const db = ctx.makeDb(env);
3456
4524
  const secret = await getVaultSecret(db, "network_share_secret");
3457
4525
  if (!secret) return json({ error: "unauthorized" }, 401);
3458
- const verified = await (0, import_db5.verifyFederatedRequest)(req, { secret });
4526
+ const verified = await (0, import_db6.verifyFederatedRequest)(req, { secret });
3459
4527
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
3460
4528
  const types = await Promise.all(
3461
4529
  Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm6.typeSummary)({ crm: ctx.chapter.crm, db }, type))
@@ -3473,7 +4541,7 @@ async function snapshotOne(db, env, ctx, target) {
3473
4541
  if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
3474
4542
  const destination = new URL("/api/network/snapshot", target.url);
3475
4543
  try {
3476
- const headers = await (0, import_db5.signFederatedRequest)({
4544
+ const headers = await (0, import_db6.signFederatedRequest)({
3477
4545
  secret,
3478
4546
  sender: ctx.chapter.id,
3479
4547
  method: "GET",
@@ -3514,7 +4582,7 @@ async function snapshotOne(db, env, ctx, target) {
3514
4582
  }
3515
4583
  var handleAdminNetworkRollup = async (req, url, env, ctx) => {
3516
4584
  if (req.method !== "GET" || url.pathname !== "/api/admin/network/rollup") return null;
3517
- const got = await gate4(req, env, ctx);
4585
+ const got = await gate5(req, env, ctx);
3518
4586
  if ("response" in got) return got.response;
3519
4587
  const targets = await Promise.all(
3520
4588
  ctx.chapter.network.targets.map((target) => snapshotOne(got.db, env, ctx, target))
@@ -3541,7 +4609,7 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
3541
4609
  };
3542
4610
  var handleAdminNetworkPush = async (req, url, env, ctx) => {
3543
4611
  if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
3544
- const got = await gate4(req, env, ctx);
4612
+ const got = await gate5(req, env, ctx);
3545
4613
  if ("response" in got) return got.response;
3546
4614
  let body;
3547
4615
  try {
@@ -3556,22 +4624,22 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
3556
4624
  if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
3557
4625
  const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
3558
4626
  if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
3559
- const record = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
3560
- if (!record) return json({ error: "record not found" }, 404);
4627
+ const record2 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
4628
+ if (!record2) return json({ error: "record not found" }, 404);
3561
4629
  const results = await Promise.all(
3562
- targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record))
4630
+ targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record2))
3563
4631
  );
3564
4632
  return json({ ok: results.every((result) => result.ok), results });
3565
4633
  };
3566
4634
 
3567
4635
  // src/worker-routes-network-records.ts
3568
4636
  var import_crm7 = require("@odla-ai/crm");
3569
- var import_db6 = require("@odla-ai/db");
4637
+ var import_db7 = require("@odla-ai/db");
3570
4638
  var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
3571
4639
  var MAX_PAGE = 50;
3572
4640
  var MAX_OFFSET = 1e4;
3573
4641
  var MAX_SEARCH = 200;
3574
- async function gate5(req, env, ctx) {
4642
+ async function gate6(req, env, ctx) {
3575
4643
  const db = ctx.makeDb(env);
3576
4644
  const user = await ctx.verifyUser(req, env);
3577
4645
  if (!user) return { response: json({ error: "unauthorized" }, 401) };
@@ -3584,18 +4652,18 @@ function integer(value, fallback, min, max) {
3584
4652
  const parsed = Number(value);
3585
4653
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
3586
4654
  }
3587
- function projectRecord(record, allowed) {
4655
+ function projectRecord(record2, allowed) {
3588
4656
  const fields = Object.fromEntries(
3589
- allowed.flatMap((name) => Object.prototype.hasOwnProperty.call(record.fields, name) ? [[name, record.fields[name]]] : [])
4657
+ allowed.flatMap((name) => Object.prototype.hasOwnProperty.call(record2.fields, name) ? [[name, record2.fields[name]]] : [])
3590
4658
  );
3591
4659
  return {
3592
- id: record.id,
3593
- type: record.type,
3594
- name: record.name,
3595
- ...record.stage ? { stage: record.stage } : {},
4660
+ id: record2.id,
4661
+ type: record2.type,
4662
+ name: record2.name,
4663
+ ...record2.stage ? { stage: record2.stage } : {},
3596
4664
  fields,
3597
- createdAt: record.createdAt,
3598
- updatedAt: record.updatedAt
4665
+ createdAt: record2.createdAt,
4666
+ updatedAt: record2.updatedAt
3599
4667
  };
3600
4668
  }
3601
4669
  var handleNetworkRecords = async (req, url, env, ctx) => {
@@ -3604,7 +4672,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
3604
4672
  const secret = await getVaultSecret(db, "network_share_secret");
3605
4673
  const readers = ctx.chapter.network.readers;
3606
4674
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
3607
- const verified = await (0, import_db6.verifyFederatedRequest)(req, {
4675
+ const verified = await (0, import_db7.verifyFederatedRequest)(req, {
3608
4676
  secret,
3609
4677
  senders: readers.map((reader2) => reader2.id)
3610
4678
  });
@@ -3633,7 +4701,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
3633
4701
  version: 1,
3634
4702
  site: { id: ctx.chapter.id, name: ctx.chapter.name },
3635
4703
  type,
3636
- records: page.records.map((record) => projectRecord(record, allowed)),
4704
+ records: page.records.map((record2) => projectRecord(record2, allowed)),
3637
4705
  total: page.total,
3638
4706
  limit: page.limit,
3639
4707
  offset: page.offset
@@ -3649,7 +4717,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
3649
4717
  const destination = new URL("/api/network/records", target.url);
3650
4718
  destination.search = query.toString();
3651
4719
  try {
3652
- const headers = await (0, import_db6.signFederatedRequest)({
4720
+ const headers = await (0, import_db7.signFederatedRequest)({
3653
4721
  secret,
3654
4722
  sender: ctx.chapter.id,
3655
4723
  method: "GET",
@@ -3674,7 +4742,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
3674
4742
  }
3675
4743
  var handleAdminNetworkRecords = async (req, url, env, ctx) => {
3676
4744
  if (req.method !== "GET" || url.pathname !== "/api/admin/network/records") return null;
3677
- const got = await gate5(req, env, ctx);
4745
+ const got = await gate6(req, env, ctx);
3678
4746
  if ("response" in got) return got.response;
3679
4747
  const targetId = url.searchParams.get("targetId") ?? "";
3680
4748
  const type = url.searchParams.get("type") ?? "";
@@ -3701,8 +4769,8 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
3701
4769
  // src/worker-routes-network-notes.ts
3702
4770
  var IDENTIFIER2 = /^[a-z][a-zA-Z0-9_]*$/;
3703
4771
  var RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
3704
- var MAX_NOTE = 4e3;
3705
- async function gate6(req, env, ctx) {
4772
+ var MAX_NOTE2 = 4e3;
4773
+ async function gate7(req, env, ctx) {
3706
4774
  const db = ctx.makeDb(env);
3707
4775
  const user = await ctx.verifyUser(req, env);
3708
4776
  if (!user) return { response: json({ error: "unauthorized" }, 401) };
@@ -3736,7 +4804,7 @@ function asNote(row) {
3736
4804
  }
3737
4805
  var handleAdminNetworkNotes = async (req, url, env, ctx) => {
3738
4806
  if (url.pathname !== "/api/admin/network/notes" || req.method !== "GET" && req.method !== "POST") return null;
3739
- const got = await gate6(req, env, ctx);
4807
+ const got = await gate7(req, env, ctx);
3740
4808
  if ("response" in got) return got.response;
3741
4809
  const identity = noteIdentity(url, ctx);
3742
4810
  if (!identity) return json({ error: "valid targetId, type, and recordId are required" }, 400);
@@ -3753,7 +4821,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
3753
4821
  return json({ notes: (result.networkNotes ?? []).map((row) => asNote(row)) });
3754
4822
  }
3755
4823
  const contentLength = Number(req.headers.get("content-length") ?? 0);
3756
- if (contentLength > MAX_NOTE * 2) return json({ error: "note is too large" }, 413);
4824
+ if (contentLength > MAX_NOTE2 * 2) return json({ error: "note is too large" }, 413);
3757
4825
  let input;
3758
4826
  try {
3759
4827
  input = await req.json();
@@ -3761,7 +4829,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
3761
4829
  return json({ error: "invalid JSON body" }, 400);
3762
4830
  }
3763
4831
  const body = typeof input.body === "string" ? input.body.trim() : "";
3764
- if (!body || body.length > MAX_NOTE) return json({ error: `note must be 1\u2013${MAX_NOTE} characters` }, 400);
4832
+ if (!body || body.length > MAX_NOTE2) return json({ error: `note must be 1\u2013${MAX_NOTE2} characters` }, 400);
3765
4833
  const id = crypto.randomUUID();
3766
4834
  const note = {
3767
4835
  id,
@@ -3784,13 +4852,13 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
3784
4852
 
3785
4853
  // src/worker-routes-network-shared-notes.ts
3786
4854
  var import_crm8 = require("@odla-ai/crm");
3787
- var import_db7 = require("@odla-ai/db");
4855
+ var import_db8 = require("@odla-ai/db");
3788
4856
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
3789
4857
  var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
3790
4858
  var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
3791
- var MAX_NOTE2 = 4e3;
4859
+ var MAX_NOTE3 = 4e3;
3792
4860
  var NETWORK_META = "chapterNetwork";
3793
- async function gate7(req, env, ctx) {
4861
+ async function gate8(req, env, ctx) {
3794
4862
  const db = ctx.makeDb(env);
3795
4863
  const user = await ctx.verifyUser(req, env);
3796
4864
  if (!user) return { response: json({ error: "unauthorized" }, 401) };
@@ -3836,14 +4904,14 @@ function sourceLabel(sourceId) {
3836
4904
  }
3837
4905
  var handleNetworkSharedNotes = async (req, url, env, ctx) => {
3838
4906
  if (url.pathname !== "/api/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
3839
- if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_NOTE2 * 2) {
4907
+ if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_NOTE3 * 2) {
3840
4908
  return json({ error: "note is too large" }, 413);
3841
4909
  }
3842
4910
  const db = ctx.makeDb(env);
3843
4911
  const secret = await getVaultSecret(db, "network_share_secret");
3844
4912
  const readers = ctx.chapter.network.readers;
3845
4913
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
3846
- const verified = await (0, import_db7.verifyFederatedRequest)(req, {
4914
+ const verified = await (0, import_db8.verifyFederatedRequest)(req, {
3847
4915
  secret,
3848
4916
  senders: readers.map((reader2) => reader2.id)
3849
4917
  });
@@ -3854,8 +4922,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
3854
4922
  if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
3855
4923
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
3856
4924
  }
3857
- const record = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
3858
- if (!record || record.type !== recordType) return json({ error: "record not found" }, 404);
4925
+ const record2 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
4926
+ if (!record2 || record2.type !== recordType) return json({ error: "record not found" }, 404);
3859
4927
  if (req.method === "GET") {
3860
4928
  return json({
3861
4929
  version: 1,
@@ -3871,8 +4939,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
3871
4939
  }
3872
4940
  const body = typeof input.body === "string" ? input.body.trim() : "";
3873
4941
  const mutationId = typeof input.mutationId === "string" ? input.mutationId : "";
3874
- if (!body || body.length > MAX_NOTE2 || !MUTATION_ID.test(mutationId)) {
3875
- return json({ error: `body must be 1\u2013${MAX_NOTE2} characters and mutationId must be valid` }, 400);
4942
+ if (!body || body.length > MAX_NOTE3 || !MUTATION_ID.test(mutationId)) {
4943
+ return json({ error: `body must be 1\u2013${MAX_NOTE3} characters and mutationId must be valid` }, 400);
3876
4944
  }
3877
4945
  const createdAt = Date.now();
3878
4946
  const id = `network:${verified.sender}:${mutationId}`;
@@ -3914,7 +4982,7 @@ function targetFor2(ctx, id) {
3914
4982
  }
3915
4983
  var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
3916
4984
  if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
3917
- const got = await gate7(req, env, ctx);
4985
+ const got = await gate8(req, env, ctx);
3918
4986
  if ("response" in got) return got.response;
3919
4987
  const targetId = url.searchParams.get("targetId") ?? "";
3920
4988
  const recordType = url.searchParams.get("type") ?? "";
@@ -3932,7 +5000,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
3932
5000
  return json({ error: "invalid JSON body" }, 400);
3933
5001
  }
3934
5002
  const note = typeof input.body === "string" ? input.body.trim() : "";
3935
- if (!note || note.length > MAX_NOTE2) return json({ error: `note must be 1\u2013${MAX_NOTE2} characters` }, 400);
5003
+ if (!note || note.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
3936
5004
  body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
3937
5005
  }
3938
5006
  const secret = await getVaultSecret(got.db, target.secretName);
@@ -3940,7 +5008,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
3940
5008
  const destination = new URL("/api/network/shared-notes", target.url);
3941
5009
  destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
3942
5010
  try {
3943
- const headers = await (0, import_db7.signFederatedRequest)({
5011
+ const headers = await (0, import_db8.signFederatedRequest)({
3944
5012
  secret,
3945
5013
  sender: ctx.chapter.id,
3946
5014
  method: req.method,
@@ -4061,11 +5129,11 @@ var handleFormation = async (req, url, env, ctx) => {
4061
5129
  };
4062
5130
 
4063
5131
  // src/signup-control-delivery.ts
4064
- var import_db8 = require("@odla-ai/db");
5132
+ var import_db9 = require("@odla-ai/db");
4065
5133
  async function deliverSignupControl(options) {
4066
5134
  const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
4067
5135
  const body = JSON.stringify(options.envelope);
4068
- const signed = await (0, import_db8.signFederatedRequest)({
5136
+ const signed = await (0, import_db9.signFederatedRequest)({
4069
5137
  secret: options.secret,
4070
5138
  sender: options.sender,
4071
5139
  method: "POST",
@@ -4085,6 +5153,31 @@ async function deliverSignupControl(options) {
4085
5153
  return result;
4086
5154
  }
4087
5155
 
5156
+ // src/admission-grants-delivery.ts
5157
+ var import_db10 = require("@odla-ai/db");
5158
+ async function deliverAdmissionGrants(options) {
5159
+ const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
5160
+ const body = JSON.stringify(options.envelope);
5161
+ const signed = await (0, import_db10.signFederatedRequest)({
5162
+ secret: options.secret,
5163
+ sender: options.sender,
5164
+ method: "POST",
5165
+ url: destination,
5166
+ body
5167
+ });
5168
+ const response = await (options.fetch ?? globalThis.fetch)(destination, {
5169
+ method: "POST",
5170
+ headers: { ...signed, "content-type": "application/json" },
5171
+ body
5172
+ });
5173
+ const result = await response.json();
5174
+ if (!response.ok || !("ok" in result) || result.ok !== true) {
5175
+ const failure = result;
5176
+ throw new Error(`admission grants rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
5177
+ }
5178
+ return result;
5179
+ }
5180
+
4088
5181
  // src/signup-control-theme.ts
4089
5182
  var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
4090
5183
  var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
@@ -4224,11 +5317,13 @@ var BUILTIN_ROUTES = [
4224
5317
  handleCrm,
4225
5318
  handleNetworkShared,
4226
5319
  handleSignupControl,
5320
+ handleAdmissionGrants,
4227
5321
  handleNetworkSnapshot,
4228
5322
  handleNetworkRecords,
4229
5323
  handleNetworkSharedNotes,
4230
5324
  handleFormation,
4231
5325
  handleMember,
5326
+ handleGifts,
4232
5327
  handleSchedule,
4233
5328
  handlePayments,
4234
5329
  handleAdminMeetings,
@@ -4238,6 +5333,7 @@ var BUILTIN_ROUTES = [
4238
5333
  handleAdminPeopleAccess,
4239
5334
  handleAdminPeopleRole,
4240
5335
  handleAdminCrmSync,
5336
+ handleAdminAdmissionGrants,
4241
5337
  handleAdminClerkPrivateProfileSync,
4242
5338
  // Aggregation
4243
5339
  handleAdminDashboard,