@odla-ai/chapter 0.37.0 → 0.38.1

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.
@@ -199,204 +199,10 @@ async function applicationIdForSubmission(submissionId) {
199
199
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
200
200
  }
201
201
 
202
- // src/admission-grants-contract.ts
203
- var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
204
- var ADMISSION_GRANTS_PATH = "/api/admission-grants";
205
-
206
- // src/signup-control-canonical.ts
207
- var encoder = new TextEncoder();
208
- function canonicalValue(value) {
209
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
210
- return JSON.stringify(value);
211
- }
212
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
213
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
214
- const record3 = value;
215
- const entries = Object.keys(record3).sort().flatMap((key) => {
216
- const item = record3[key];
217
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
218
- });
219
- return `{${entries.join(",")}}`;
220
- }
221
- function canonicalSignupControl(value) {
222
- return canonicalValue(value);
223
- }
224
- async function signupControlDigest(document) {
225
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
226
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
227
- }
228
-
229
- // src/admission-grants.ts
230
- var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
231
- var MAX_GRANTS = 2e3;
232
- var MAX_NOTE = 2e3;
233
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
234
- var isAdmissionEmail = (value) => EMAIL_RE.test(value);
235
- function normalizeAdmissionEmail(email) {
202
+ // src/email-identity.ts
203
+ function normalizeChapterEmail(email) {
236
204
  return email.trim().toLowerCase();
237
205
  }
238
- function validateAdmissionGrantDocument(input) {
239
- const errors = [];
240
- const root = input && typeof input === "object" ? input : {};
241
- if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
242
- const revision = Number(root.revision);
243
- const generatedAt = Number(root.generatedAt);
244
- if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
245
- if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
246
- const source = root.source && typeof root.source === "object" ? root.source : {};
247
- const target = root.target && typeof root.target === "object" ? root.target : {};
248
- const sourceId = typeof source.appId === "string" ? source.appId : "";
249
- const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
250
- const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
251
- const environment = typeof target.environment === "string" ? target.environment : "";
252
- if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
253
- if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
254
- if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
255
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
256
- if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
257
- const rawGrants = Array.isArray(root.grants) ? root.grants : [];
258
- if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
259
- const grants = [];
260
- const seenIds = /* @__PURE__ */ new Set();
261
- const seenEmails = /* @__PURE__ */ new Set();
262
- for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
263
- const grant = raw && typeof raw === "object" ? raw : {};
264
- const id = typeof grant.id === "string" ? grant.id : "";
265
- const email = typeof grant.email === "string" ? grant.email.trim() : "";
266
- const normalized = normalizeAdmissionEmail(email);
267
- const decision = grant.decision;
268
- const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
269
- const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
270
- if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
271
- if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
272
- if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
273
- if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
274
- if (decision !== "waive_interview" && decision !== "require_interview") {
275
- errors.push(`document.grants[${index}].decision is invalid`);
276
- }
277
- if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
278
- if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
279
- errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
280
- }
281
- seenIds.add(id);
282
- seenEmails.add(normalized);
283
- grants.push({
284
- id,
285
- email,
286
- decision,
287
- ...note ? { note } : {},
288
- ...expiresAt !== void 0 ? { expiresAt } : {}
289
- });
290
- }
291
- if (errors.length) return { ok: false, errors };
292
- return {
293
- ok: true,
294
- value: {
295
- schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
296
- revision,
297
- generatedAt,
298
- source: { appId: sourceId, actor: sourceActor },
299
- target: { chapterId, environment },
300
- grants
301
- }
302
- };
303
- }
304
- var rows = (value) => Array.isArray(value) ? value : [];
305
- function asGrant(row) {
306
- if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
307
- if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
308
- if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
309
- return row;
310
- }
311
- async function findAdmissionGrant(db, targetId, email, now) {
312
- const emailNormalized = normalizeAdmissionEmail(email);
313
- if (!isAdmissionEmail(emailNormalized)) return null;
314
- const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
315
- const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
316
- grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
317
- )).sort((a, b) => {
318
- const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
319
- return scope || b.createdAt - a.createdAt;
320
- });
321
- return active[0] ?? null;
322
- }
323
- async function hasAdmissionRevision(db, sourceId, targetId, environment) {
324
- const where = { sourceId, targetId, environment };
325
- if (db.aggregate) {
326
- return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
327
- }
328
- const found = await db.query({ admissionGrantRevisions: {} });
329
- return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
330
- }
331
- async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
332
- if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
333
- const result = await db.query({ admissionGrantRevisions: { $: {
334
- where: { sourceId, targetId, environment },
335
- order: { revision: "desc" },
336
- limit: 1
337
- } } });
338
- const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
339
- return row ? row : null;
340
- }
341
- async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
342
- const { document, digest } = envelope;
343
- const current = rows((await db.query({ admissionGrants: { $: {
344
- where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
345
- limit: MAX_GRANTS
346
- } } })).admissionGrants);
347
- const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
348
- const ops = [];
349
- for (const row of current) {
350
- const id = String(row.id ?? "");
351
- if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
352
- active: false,
353
- revokedAt: receivedAt,
354
- revokedBy: document.source.actor,
355
- updatedAt: receivedAt,
356
- updatedBy: document.source.actor
357
- } });
358
- }
359
- for (const grant of document.grants) {
360
- const id = `admission:parent:${document.source.appId}:${grant.id}`;
361
- const existing = current.find((row) => row.id === id);
362
- ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
363
- id,
364
- sourceGrantId: grant.id,
365
- sourceScope: "parent",
366
- sourceId: document.source.appId,
367
- targetId: document.target.chapterId,
368
- email: grant.email,
369
- emailNormalized: normalizeAdmissionEmail(grant.email),
370
- decision: grant.decision,
371
- ...grant.note ? { note: grant.note } : {},
372
- ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
373
- active: true,
374
- sourceRevision: document.revision,
375
- createdAt: Number(existing?.createdAt ?? receivedAt),
376
- createdBy: String(existing?.createdBy ?? document.source.actor),
377
- updatedAt: receivedAt,
378
- updatedBy: document.source.actor
379
- } });
380
- const retract = [
381
- ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
382
- ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
383
- ];
384
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
385
- }
386
- if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
387
- const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
388
- ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
389
- id: revisionId,
390
- sourceId: document.source.appId,
391
- targetId: document.target.chapterId,
392
- environment: document.target.environment,
393
- revision: document.revision,
394
- digest,
395
- active: true,
396
- receivedAt
397
- } });
398
- await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
399
- }
400
206
 
401
207
  // src/member.ts
402
208
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
@@ -449,9 +255,9 @@ function resolveApplication(a) {
449
255
  validateEmail: a?.validateEmail ?? true
450
256
  };
451
257
  }
452
- var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
258
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
453
259
  function isValidEmail(value) {
454
- return typeof value === "string" && EMAIL_RE2.test(value);
260
+ return typeof value === "string" && EMAIL_RE.test(value);
455
261
  }
456
262
  function clampArray(value, max) {
457
263
  if (!Array.isArray(value)) return value;
@@ -486,7 +292,7 @@ async function submitApplication(db, chapter, fields, opts) {
486
292
  const cap = app.maxLen[f] ?? app.defaultMaxLen;
487
293
  if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
488
294
  }
489
- if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
295
+ if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
490
296
  return { ok: false, error: "email must be a valid email address" };
491
297
  }
492
298
  const acked = hasDisclaimerAck(fields);
@@ -502,7 +308,7 @@ async function submitApplication(db, chapter, fields, opts) {
502
308
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
503
309
  if (opts.groupId) row.groupId = opts.groupId;
504
310
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
505
- row.emailNormalized = normalizeAdmissionEmail(fields.email);
311
+ row.emailNormalized = normalizeChapterEmail(fields.email);
506
312
  }
507
313
  if (opts.admissionGrant?.decision === "waive_interview") {
508
314
  row.admissionGrantId = opts.admissionGrant.id;
@@ -730,6 +536,281 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
730
536
  };
731
537
  }
732
538
 
539
+ // src/admission-grants-contract.ts
540
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
541
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
542
+
543
+ // src/signup-control-canonical.ts
544
+ var encoder = new TextEncoder();
545
+ function canonicalValue(value) {
546
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
547
+ return JSON.stringify(value);
548
+ }
549
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
550
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
551
+ const record3 = value;
552
+ const entries = Object.keys(record3).sort().flatMap((key) => {
553
+ const item = record3[key];
554
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
555
+ });
556
+ return `{${entries.join(",")}}`;
557
+ }
558
+ function canonicalSignupControl(value) {
559
+ return canonicalValue(value);
560
+ }
561
+ async function signupControlDigest(document) {
562
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
563
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
564
+ }
565
+
566
+ // src/admission-grants.ts
567
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
568
+ var MAX_GRANTS = 2e3;
569
+ var MAX_NOTE = 2e3;
570
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
571
+ var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
572
+ function normalizeAdmissionEmail(email) {
573
+ return normalizeChapterEmail(email);
574
+ }
575
+ function validateAdmissionGrantDocument(input) {
576
+ const errors = [];
577
+ const root = input && typeof input === "object" ? input : {};
578
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
579
+ const revision = Number(root.revision);
580
+ const generatedAt = Number(root.generatedAt);
581
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
582
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
583
+ const source = root.source && typeof root.source === "object" ? root.source : {};
584
+ const target = root.target && typeof root.target === "object" ? root.target : {};
585
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
586
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
587
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
588
+ const environment = typeof target.environment === "string" ? target.environment : "";
589
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
590
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
591
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
592
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
593
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
594
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
595
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
596
+ const grants = [];
597
+ const seenIds = /* @__PURE__ */ new Set();
598
+ const seenEmails = /* @__PURE__ */ new Set();
599
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
600
+ const grant = raw && typeof raw === "object" ? raw : {};
601
+ const id = typeof grant.id === "string" ? grant.id : "";
602
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
603
+ const normalized = normalizeAdmissionEmail(email);
604
+ const decision = grant.decision;
605
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
606
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
607
+ if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
608
+ if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
609
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
610
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
611
+ if (decision !== "waive_interview" && decision !== "require_interview") {
612
+ errors.push(`document.grants[${index}].decision is invalid`);
613
+ }
614
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
615
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
616
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
617
+ }
618
+ seenIds.add(id);
619
+ seenEmails.add(normalized);
620
+ grants.push({
621
+ id,
622
+ email,
623
+ decision,
624
+ ...note ? { note } : {},
625
+ ...expiresAt !== void 0 ? { expiresAt } : {}
626
+ });
627
+ }
628
+ if (errors.length) return { ok: false, errors };
629
+ return {
630
+ ok: true,
631
+ value: {
632
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
633
+ revision,
634
+ generatedAt,
635
+ source: { appId: sourceId, actor: sourceActor },
636
+ target: { chapterId, environment },
637
+ grants
638
+ }
639
+ };
640
+ }
641
+ var rows = (value) => Array.isArray(value) ? value : [];
642
+ function asGrant(row) {
643
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
644
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
645
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
646
+ return row;
647
+ }
648
+ async function findAdmissionGrant(db, targetId, email, now) {
649
+ const emailNormalized = normalizeAdmissionEmail(email);
650
+ if (!isAdmissionEmail(emailNormalized)) return null;
651
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
652
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
653
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
654
+ )).sort((a, b) => {
655
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
656
+ return scope || b.createdAt - a.createdAt;
657
+ });
658
+ return active[0] ?? null;
659
+ }
660
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
661
+ const where = { sourceId, targetId, environment };
662
+ if (db.aggregate) {
663
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
664
+ }
665
+ const found = await db.query({ admissionGrantRevisions: {} });
666
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
667
+ }
668
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
669
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
670
+ const result = await db.query({ admissionGrantRevisions: { $: {
671
+ where: { sourceId, targetId, environment },
672
+ order: { revision: "desc" },
673
+ limit: 1
674
+ } } });
675
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
676
+ return row ? row : null;
677
+ }
678
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
679
+ const { document, digest } = envelope;
680
+ const current = rows((await db.query({ admissionGrants: { $: {
681
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
682
+ limit: MAX_GRANTS
683
+ } } })).admissionGrants);
684
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
685
+ const ops = [];
686
+ for (const row of current) {
687
+ const id = String(row.id ?? "");
688
+ if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
689
+ active: false,
690
+ revokedAt: receivedAt,
691
+ revokedBy: document.source.actor,
692
+ updatedAt: receivedAt,
693
+ updatedBy: document.source.actor
694
+ } });
695
+ }
696
+ for (const grant of document.grants) {
697
+ const id = `admission:parent:${document.source.appId}:${grant.id}`;
698
+ const existing = current.find((row) => row.id === id);
699
+ ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
700
+ id,
701
+ sourceGrantId: grant.id,
702
+ sourceScope: "parent",
703
+ sourceId: document.source.appId,
704
+ targetId: document.target.chapterId,
705
+ email: grant.email,
706
+ emailNormalized: normalizeAdmissionEmail(grant.email),
707
+ decision: grant.decision,
708
+ ...grant.note ? { note: grant.note } : {},
709
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
710
+ active: true,
711
+ sourceRevision: document.revision,
712
+ createdAt: Number(existing?.createdAt ?? receivedAt),
713
+ createdBy: String(existing?.createdBy ?? document.source.actor),
714
+ updatedAt: receivedAt,
715
+ updatedBy: document.source.actor
716
+ } });
717
+ const retract = [
718
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
719
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
720
+ ];
721
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
722
+ }
723
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
724
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
725
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
726
+ id: revisionId,
727
+ sourceId: document.source.appId,
728
+ targetId: document.target.chapterId,
729
+ environment: document.target.environment,
730
+ revision: document.revision,
731
+ digest,
732
+ active: true,
733
+ receivedAt
734
+ } });
735
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
736
+ }
737
+
738
+ // src/flow-fields.ts
739
+ var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
740
+ var APPLICATION_LABELS = {
741
+ firstName: "First name",
742
+ lastName: "Last name",
743
+ email: "Email",
744
+ referral: "Referral source",
745
+ referralName: "Referral name",
746
+ whoYouAre: "Who you are",
747
+ linkedin: "LinkedIn",
748
+ message: "Intro message",
749
+ phone: "Phone",
750
+ state: "State"
751
+ };
752
+ function humanize(id) {
753
+ const text3 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
754
+ return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id;
755
+ }
756
+ function applicationDefs(chapter) {
757
+ try {
758
+ return chapter.crm.type("person").fields;
759
+ } catch {
760
+ return {};
761
+ }
762
+ }
763
+ function applicationType(id, def) {
764
+ if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
765
+ return id === "email" ? "email" : id === "linkedin" ? "url" : "string";
766
+ }
767
+ function applicationFlowFields(chapter, application = chapter.application) {
768
+ const defs = applicationDefs(chapter);
769
+ return [...application.required, ...application.optional].map((id, order) => {
770
+ const def = defs[id];
771
+ const conditions = application.conditions[id];
772
+ const type = applicationType(id, def);
773
+ return {
774
+ id,
775
+ label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
776
+ type,
777
+ required: application.required.includes(id),
778
+ order,
779
+ maxLength: application.maxLen[id] ?? application.defaultMaxLen,
780
+ ...type === "enum" && def?.options ? { options: [...def.options] } : {},
781
+ ...conditions ? { conditions: { ...conditions } } : {},
782
+ ...type === "email" ? { emailIdentity: "literal" } : {}
783
+ };
784
+ });
785
+ }
786
+ function resolveFormationFlowFields(crm, formation) {
787
+ if (!formation.enabled) return [];
788
+ const def = crm.type(formation.type);
789
+ return [...formation.required, ...formation.optional].map((id, order) => {
790
+ const field = def.fields[id];
791
+ return {
792
+ id,
793
+ label: field.label ?? humanize(id),
794
+ type: field.type,
795
+ required: formation.required.includes(id),
796
+ order,
797
+ maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
798
+ ...field.options ? { options: [...field.options] } : {},
799
+ ...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
800
+ };
801
+ });
802
+ }
803
+ function formationFlowFields(chapter, formation = chapter.formation) {
804
+ return resolveFormationFlowFields(chapter.crm, formation);
805
+ }
806
+ function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
807
+ return {
808
+ schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
809
+ flow,
810
+ fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
811
+ };
812
+ }
813
+
733
814
  // src/gift-offers.ts
734
815
  var GIFT_OFFER_CATALOG_VERSION = 1;
735
816
  var MAX_GIFT_OFFERS = 20;
@@ -1144,10 +1225,10 @@ async function syncApplicationToCrm(deps, opts) {
1144
1225
  return recordId;
1145
1226
  }
1146
1227
  async function backfillCrm(deps) {
1147
- const [appsRes, usersRes] = await Promise.all([
1148
- deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1149
- deps.db.query({ $users: { $: { limit: 1e3 } } })
1150
- ]);
1228
+ const data = await deps.db.query({
1229
+ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
1230
+ $users: { $: { limit: 1e3 } }
1231
+ });
1151
1232
  const seen = /* @__PURE__ */ new Set();
1152
1233
  let synced = 0;
1153
1234
  const errors = [];
@@ -1162,8 +1243,8 @@ async function backfillCrm(deps) {
1162
1243
  errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
1163
1244
  }
1164
1245
  };
1165
- for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1166
- for (const u of usersRes.$users ?? []) {
1246
+ for (const a of data.applications ?? []) await run(a, str(a.status));
1247
+ for (const u of data.$users ?? []) {
1167
1248
  if (u.deleted === true) continue;
1168
1249
  await run({ email: u.email, firstName: str(u.name) });
1169
1250
  }
@@ -1735,7 +1816,7 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
1735
1816
  // src/worker-routes.ts
1736
1817
  async function memberSessionApplication(db, chapterId, email) {
1737
1818
  let apps = (await db.query({ applications: { $: {
1738
- where: { emailNormalized: normalizeAdmissionEmail(email) },
1819
+ where: { emailNormalized: normalizeChapterEmail(email) },
1739
1820
  limit: 20
1740
1821
  } } })).applications;
1741
1822
  if (!apps?.length) apps = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
@@ -1834,6 +1915,7 @@ var handleMember = async (req, url, env, ctx) => {
1834
1915
  const application = controlledApplication(chapter.application, activeControl?.document);
1835
1916
  return json({
1836
1917
  ...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
1918
+ fieldContract: chapterFlowFieldContract(chapter, "join", application),
1837
1919
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
1838
1920
  ...activeControl ? { signupControl: {
1839
1921
  revision: activeControl.revision,
@@ -3077,7 +3159,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
3077
3159
  return json({ error: "invalid JSON body" }, 400);
3078
3160
  }
3079
3161
  const email = typeof body.email === "string" ? body.email.trim() : "";
3080
- const emailNormalized = normalizeAdmissionEmail(email);
3162
+ const emailNormalized = normalizeChapterEmail(email);
3081
3163
  if (!isValidEmail(emailNormalized)) return json({ error: "email must be valid" }, 400);
3082
3164
  const decision = body.decision ?? "waive_interview";
3083
3165
  if (decision !== "waive_interview" && decision !== "require_interview") {
@@ -3579,7 +3661,7 @@ async function rotateGiftClaim(db, claim, now) {
3579
3661
  async function redeemGiftClaim(db, claim, beneficiary, now) {
3580
3662
  if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) return null;
3581
3663
  const chapterId = String(claim.chapterId);
3582
- const emailNormalized = normalizeAdmissionEmail(beneficiary.email);
3664
+ const emailNormalized = normalizeChapterEmail(beneficiary.email);
3583
3665
  const target = claim.target;
3584
3666
  const ops = [];
3585
3667
  let result;
@@ -3876,7 +3958,7 @@ async function startSubscription(req, env, ctx) {
3876
3958
  offer,
3877
3959
  purchaserType: "application",
3878
3960
  purchaserEmail: String(app.email ?? ""),
3879
- purchaserEmailNormalized: normalizeAdmissionEmail(String(app.email ?? "")),
3961
+ purchaserEmailNormalized: normalizeChapterEmail(String(app.email ?? "")),
3880
3962
  purchaserApplicationId: applicationId,
3881
3963
  stripeSecret: secretKey,
3882
3964
  publishableKey: group.stripePublishableKey,
@@ -4025,7 +4107,7 @@ var handleGifts = async (req, url, env, ctx) => {
4025
4107
  if (!user) return json({ error: "unauthorized" }, 401);
4026
4108
  const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4027
4109
  const found = await db.query({ giftClaims: { $: { where: {
4028
- purchaserEmailNormalized: normalizeAdmissionEmail(user.email),
4110
+ purchaserEmailNormalized: normalizeChapterEmail(user.email),
4029
4111
  chapterId: ctx.chapter.id
4030
4112
  }, limit: 200 } } });
4031
4113
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
@@ -4052,7 +4134,7 @@ var handleGifts = async (req, url, env, ctx) => {
4052
4134
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4053
4135
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
4054
4136
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
4055
- const emailNormalized = normalizeAdmissionEmail(user.email);
4137
+ const emailNormalized = normalizeChapterEmail(user.email);
4056
4138
  const applications = rows2((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
4057
4139
  const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
4058
4140
  try {
@@ -4094,7 +4176,7 @@ var handleGifts = async (req, url, env, ctx) => {
4094
4176
  return invalid();
4095
4177
  }
4096
4178
  }
4097
- const owns = user ? claim.purchaserEmailNormalized === normalizeAdmissionEmail(user.email) : claim.purchaserApplicationId === applicationId;
4179
+ const owns = user ? claim.purchaserEmailNormalized === normalizeChapterEmail(user.email) : claim.purchaserApplicationId === applicationId;
4098
4180
  if (!owns) return invalid();
4099
4181
  const rotated = await rotateGiftClaim(db, claim, Date.now());
4100
4182
  return rotated ? json(rotated) : invalid();
@@ -4287,14 +4369,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
4287
4369
  if (gate9 instanceof Response) return gate9;
4288
4370
  const { db } = gate9;
4289
4371
  const sk = await getVaultSecret(db, "clerk_secret_key");
4290
- const [appsRes, usersRes, roleList] = await Promise.all([
4291
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
4292
- db.query({ $users: { $: { limit: 200 } } }),
4372
+ const [data, roleList] = await Promise.all([
4373
+ db.query({
4374
+ applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
4375
+ $users: { $: { limit: 200 } }
4376
+ }),
4293
4377
  sk ? clerkListUsers(sk).catch(() => []) : Promise.resolve([])
4294
4378
  ]);
4295
4379
  const clerkByUserId = new Map(roleList.map((u) => [u.id, u]));
4296
4380
  const people = /* @__PURE__ */ new Map();
4297
- for (const u of usersRes.$users ?? []) {
4381
+ for (const u of data.$users ?? []) {
4298
4382
  if (u.deleted === true) continue;
4299
4383
  const email = typeof u.email === "string" ? u.email : "";
4300
4384
  if (!email) continue;
@@ -4309,7 +4393,7 @@ var handleAdminPeople = async (req, url, env, ctx) => {
4309
4393
  application: null
4310
4394
  });
4311
4395
  }
4312
- for (const a of appsRes.applications ?? []) {
4396
+ for (const a of data.applications ?? []) {
4313
4397
  const key = String(a.email ?? "").toLowerCase();
4314
4398
  if (!key) continue;
4315
4399
  const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
@@ -4514,13 +4598,19 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4514
4598
  const now = Date.now();
4515
4599
  const d7 = now - 7 * 864e5;
4516
4600
  const d30 = now - 30 * 864e5;
4517
- const [appsRes, meetingsRes, recsRes, groupRes] = await Promise.all([
4518
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
4519
- db.query({ meetings: { $: { where: { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } } }),
4520
- db.query({ crm_record: { $: { where: { type: "person" }, limit: 1e3 } } }),
4521
- db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })
4522
- ]);
4523
- const apps = rows3(appsRes.applications);
4601
+ const data = await db.query({
4602
+ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
4603
+ meetings: {
4604
+ $: {
4605
+ where: { status: "scheduled" },
4606
+ order: { startAt: "asc" },
4607
+ limit: 500
4608
+ }
4609
+ },
4610
+ crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
4611
+ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
4612
+ });
4613
+ const apps = rows3(data.applications);
4524
4614
  const applications = {
4525
4615
  total: apps.length,
4526
4616
  last7: apps.filter((a) => a.createdAt >= d7).length,
@@ -4532,7 +4622,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4532
4622
  pipeline[s] = 0;
4533
4623
  pipelineDelta[s] = 0;
4534
4624
  }
4535
- for (const r of rows3(recsRes.crm_record)) {
4625
+ for (const r of rows3(data.crm_record)) {
4536
4626
  const s = r.stage;
4537
4627
  if (s in pipeline) {
4538
4628
  pipeline[s] = (pipeline[s] ?? 0) + 1;
@@ -4540,7 +4630,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4540
4630
  if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
4541
4631
  }
4542
4632
  }
4543
- const meetings = rows3(meetingsRes.meetings);
4633
+ const meetings = rows3(data.meetings);
4544
4634
  const appById = new Map(apps.map((a) => [a.id, a]));
4545
4635
  const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
4546
4636
  const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
@@ -4556,7 +4646,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4556
4646
  email: a?.email ?? null
4557
4647
  };
4558
4648
  });
4559
- const group = groupRes.groups?.[0];
4649
+ const group = data.groups?.[0];
4560
4650
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
4561
4651
  let revenue = { billingReady: false };
4562
4652
  let revenueSeries = null;
@@ -4590,16 +4680,18 @@ var handleAdminBilling = async (req, url, env, ctx) => {
4590
4680
  const db = got;
4591
4681
  const sk = await getVaultSecret(db, "stripe_secret_key");
4592
4682
  if (!sk) return json({ billingReady: false, rows: [], summary: null });
4593
- const [appsRes, subsRes, groupRes] = await Promise.all([
4594
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
4595
- stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" }),
4596
- db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })
4683
+ const [data, subsRes] = await Promise.all([
4684
+ db.query({
4685
+ applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
4686
+ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
4687
+ }),
4688
+ stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" })
4597
4689
  ]);
4598
- const testMode = String(groupRes.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
4690
+ const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
4599
4691
  if (!subsRes.ok) return json({ error: "billing lookup failed upstream" }, 502);
4600
4692
  const subById = new Map(rows3(subsRes.body.data).map((s) => [s.id, s]));
4601
4693
  const billingRows = [];
4602
- for (const a of rows3(appsRes.applications)) {
4694
+ for (const a of rows3(data.applications)) {
4603
4695
  if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
4604
4696
  const sub = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
4605
4697
  const items = sub?.items?.data ?? [];
@@ -4939,21 +5031,27 @@ var handleAdminComms = async (req, url, env, ctx) => {
4939
5031
  if (got instanceof Response) return got;
4940
5032
  const db = got;
4941
5033
  const appId = m[1];
4942
- const [emailRes, meetingRes, appRes] = await Promise.all([
4943
- db.query({ emailLog: { $: { where: { applicationId: appId }, order: { sentAt: "desc" }, limit: 100 } } }),
4944
- db.query({ meetings: { $: { where: { applicationId: appId } } } }),
4945
- db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })
4946
- ]);
4947
- const app = appRes.applications?.[0] ?? null;
5034
+ const data = await db.query({
5035
+ emailLog: {
5036
+ $: {
5037
+ where: { applicationId: appId },
5038
+ order: { sentAt: "desc" },
5039
+ limit: 100
5040
+ }
5041
+ },
5042
+ meetings: { $: { where: { applicationId: appId } } },
5043
+ applications: { $: { where: { id: appId }, limit: 1 } }
5044
+ });
5045
+ const app = data.applications?.[0] ?? null;
4948
5046
  const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
4949
5047
  const vars = app ? { firstName: str2(app.firstName), lastName: str2(app.lastName), email: str2(app.email), phone: str2(app.phone), state: str2(app.state), adminUrl: `${url.origin}/admin/`, membersUrl: `${url.origin}/members/` } : null;
4950
- const emails = (emailRes.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
5048
+ const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
4951
5049
  let mailBody = typeof r.body === "string" ? r.body : null;
4952
5050
  if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
4953
5051
  const channel = r.error ? "email (failed)" : r.redirected ? "email (dev-redirected)" : r.transport === "log-only" ? "email (not delivered)" : "email";
4954
5052
  return { kind: "email", channel, label: String(r.template), subject: str2(r.subject), to: r.to ?? null, body: mailBody, at: r.sentAt, error: r.error ?? null };
4955
5053
  });
4956
- const calendar = (meetingRes.meetings ?? []).map((m2) => ({
5054
+ const calendar = (data.meetings ?? []).map((m2) => ({
4957
5055
  kind: "calendar",
4958
5056
  channel: "Google Calendar",
4959
5057
  label: m2.status === "cancelled" ? "Invitation (call later cancelled)" : "Meeting invitation",
@@ -5618,31 +5716,17 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5618
5716
 
5619
5717
  // src/worker-routes-formation.ts
5620
5718
  var import_crm9 = require("@odla-ai/crm");
5621
-
5622
- // src/formation.ts
5623
- function formationFields(crm, formation) {
5624
- const def = crm.type(formation.type);
5625
- return [...formation.required, ...formation.optional].map((id) => {
5626
- const field = def.fields[id];
5627
- return {
5628
- id,
5629
- label: field.label ?? id,
5630
- type: field.type,
5631
- required: formation.required.includes(id),
5632
- ...field.options ? { options: field.options } : {}
5633
- };
5634
- });
5635
- }
5636
-
5637
- // src/worker-routes-formation.ts
5638
5719
  var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
5639
5720
  var handleFormation = async (req, url, env, ctx) => {
5640
5721
  const formation = ctx.chapter.formation;
5641
5722
  if (!formation.enabled) return null;
5642
5723
  if (req.method === "GET" && url.pathname === "/api/formation/config") {
5724
+ const fieldContract = chapterFlowFieldContract(ctx.chapter, "formation");
5643
5725
  return json({
5644
5726
  type: formation.type,
5645
- fields: formationFields(ctx.chapter.crm, formation)
5727
+ fieldContract,
5728
+ // Backward compatibility for hosts that consumed the original array.
5729
+ fields: fieldContract.fields
5646
5730
  });
5647
5731
  }
5648
5732
  if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;