@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.
@@ -167,204 +167,10 @@ async function applicationIdForSubmission(submissionId) {
167
167
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
168
168
  }
169
169
 
170
- // src/admission-grants-contract.ts
171
- var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
172
- var ADMISSION_GRANTS_PATH = "/api/admission-grants";
173
-
174
- // src/signup-control-canonical.ts
175
- var encoder = new TextEncoder();
176
- function canonicalValue(value) {
177
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
178
- return JSON.stringify(value);
179
- }
180
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
181
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
182
- const record3 = value;
183
- const entries = Object.keys(record3).sort().flatMap((key) => {
184
- const item = record3[key];
185
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
186
- });
187
- return `{${entries.join(",")}}`;
188
- }
189
- function canonicalSignupControl(value) {
190
- return canonicalValue(value);
191
- }
192
- async function signupControlDigest(document) {
193
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
194
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
195
- }
196
-
197
- // src/admission-grants.ts
198
- var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
199
- var MAX_GRANTS = 2e3;
200
- var MAX_NOTE = 2e3;
201
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
202
- var isAdmissionEmail = (value) => EMAIL_RE.test(value);
203
- function normalizeAdmissionEmail(email) {
170
+ // src/email-identity.ts
171
+ function normalizeChapterEmail(email) {
204
172
  return email.trim().toLowerCase();
205
173
  }
206
- function validateAdmissionGrantDocument(input) {
207
- const errors = [];
208
- const root = input && typeof input === "object" ? input : {};
209
- if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
210
- const revision = Number(root.revision);
211
- const generatedAt = Number(root.generatedAt);
212
- if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
213
- if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
214
- const source = root.source && typeof root.source === "object" ? root.source : {};
215
- const target = root.target && typeof root.target === "object" ? root.target : {};
216
- const sourceId = typeof source.appId === "string" ? source.appId : "";
217
- const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
218
- const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
219
- const environment = typeof target.environment === "string" ? target.environment : "";
220
- if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
221
- if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
222
- if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
223
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
224
- if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
225
- const rawGrants = Array.isArray(root.grants) ? root.grants : [];
226
- if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
227
- const grants = [];
228
- const seenIds = /* @__PURE__ */ new Set();
229
- const seenEmails = /* @__PURE__ */ new Set();
230
- for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
231
- const grant = raw && typeof raw === "object" ? raw : {};
232
- const id = typeof grant.id === "string" ? grant.id : "";
233
- const email = typeof grant.email === "string" ? grant.email.trim() : "";
234
- const normalized = normalizeAdmissionEmail(email);
235
- const decision = grant.decision;
236
- const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
237
- const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
238
- if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
239
- if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
240
- if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
241
- if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
242
- if (decision !== "waive_interview" && decision !== "require_interview") {
243
- errors.push(`document.grants[${index}].decision is invalid`);
244
- }
245
- if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
246
- if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
247
- errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
248
- }
249
- seenIds.add(id);
250
- seenEmails.add(normalized);
251
- grants.push({
252
- id,
253
- email,
254
- decision,
255
- ...note ? { note } : {},
256
- ...expiresAt !== void 0 ? { expiresAt } : {}
257
- });
258
- }
259
- if (errors.length) return { ok: false, errors };
260
- return {
261
- ok: true,
262
- value: {
263
- schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
264
- revision,
265
- generatedAt,
266
- source: { appId: sourceId, actor: sourceActor },
267
- target: { chapterId, environment },
268
- grants
269
- }
270
- };
271
- }
272
- var rows = (value) => Array.isArray(value) ? value : [];
273
- function asGrant(row) {
274
- if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
275
- if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
276
- if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
277
- return row;
278
- }
279
- async function findAdmissionGrant(db, targetId, email, now) {
280
- const emailNormalized = normalizeAdmissionEmail(email);
281
- if (!isAdmissionEmail(emailNormalized)) return null;
282
- const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
283
- const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
284
- grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
285
- )).sort((a, b) => {
286
- const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
287
- return scope || b.createdAt - a.createdAt;
288
- });
289
- return active[0] ?? null;
290
- }
291
- async function hasAdmissionRevision(db, sourceId, targetId, environment) {
292
- const where = { sourceId, targetId, environment };
293
- if (db.aggregate) {
294
- return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
295
- }
296
- const found = await db.query({ admissionGrantRevisions: {} });
297
- return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
298
- }
299
- async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
300
- if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
301
- const result = await db.query({ admissionGrantRevisions: { $: {
302
- where: { sourceId, targetId, environment },
303
- order: { revision: "desc" },
304
- limit: 1
305
- } } });
306
- const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
307
- return row ? row : null;
308
- }
309
- async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
310
- const { document, digest } = envelope;
311
- const current = rows((await db.query({ admissionGrants: { $: {
312
- where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
313
- limit: MAX_GRANTS
314
- } } })).admissionGrants);
315
- const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
316
- const ops = [];
317
- for (const row of current) {
318
- const id = String(row.id ?? "");
319
- if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
320
- active: false,
321
- revokedAt: receivedAt,
322
- revokedBy: document.source.actor,
323
- updatedAt: receivedAt,
324
- updatedBy: document.source.actor
325
- } });
326
- }
327
- for (const grant of document.grants) {
328
- const id = `admission:parent:${document.source.appId}:${grant.id}`;
329
- const existing = current.find((row) => row.id === id);
330
- ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
331
- id,
332
- sourceGrantId: grant.id,
333
- sourceScope: "parent",
334
- sourceId: document.source.appId,
335
- targetId: document.target.chapterId,
336
- email: grant.email,
337
- emailNormalized: normalizeAdmissionEmail(grant.email),
338
- decision: grant.decision,
339
- ...grant.note ? { note: grant.note } : {},
340
- ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
341
- active: true,
342
- sourceRevision: document.revision,
343
- createdAt: Number(existing?.createdAt ?? receivedAt),
344
- createdBy: String(existing?.createdBy ?? document.source.actor),
345
- updatedAt: receivedAt,
346
- updatedBy: document.source.actor
347
- } });
348
- const retract = [
349
- ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
350
- ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
351
- ];
352
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
353
- }
354
- if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
355
- const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
356
- ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
357
- id: revisionId,
358
- sourceId: document.source.appId,
359
- targetId: document.target.chapterId,
360
- environment: document.target.environment,
361
- revision: document.revision,
362
- digest,
363
- active: true,
364
- receivedAt
365
- } });
366
- await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
367
- }
368
174
 
369
175
  // src/member.ts
370
176
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
@@ -417,9 +223,9 @@ function resolveApplication(a) {
417
223
  validateEmail: a?.validateEmail ?? true
418
224
  };
419
225
  }
420
- var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
226
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
421
227
  function isValidEmail(value) {
422
- return typeof value === "string" && EMAIL_RE2.test(value);
228
+ return typeof value === "string" && EMAIL_RE.test(value);
423
229
  }
424
230
  function clampArray(value, max) {
425
231
  if (!Array.isArray(value)) return value;
@@ -454,7 +260,7 @@ async function submitApplication(db, chapter, fields, opts) {
454
260
  const cap = app.maxLen[f] ?? app.defaultMaxLen;
455
261
  if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
456
262
  }
457
- if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
263
+ if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
458
264
  return { ok: false, error: "email must be a valid email address" };
459
265
  }
460
266
  const acked = hasDisclaimerAck(fields);
@@ -470,7 +276,7 @@ async function submitApplication(db, chapter, fields, opts) {
470
276
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
471
277
  if (opts.groupId) row.groupId = opts.groupId;
472
278
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
473
- row.emailNormalized = normalizeAdmissionEmail(fields.email);
279
+ row.emailNormalized = normalizeChapterEmail(fields.email);
474
280
  }
475
281
  if (opts.admissionGrant?.decision === "waive_interview") {
476
282
  row.admissionGrantId = opts.admissionGrant.id;
@@ -698,6 +504,281 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
698
504
  };
699
505
  }
700
506
 
507
+ // src/admission-grants-contract.ts
508
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
509
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
510
+
511
+ // src/signup-control-canonical.ts
512
+ var encoder = new TextEncoder();
513
+ function canonicalValue(value) {
514
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
515
+ return JSON.stringify(value);
516
+ }
517
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
518
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
519
+ const record3 = value;
520
+ const entries = Object.keys(record3).sort().flatMap((key) => {
521
+ const item = record3[key];
522
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
523
+ });
524
+ return `{${entries.join(",")}}`;
525
+ }
526
+ function canonicalSignupControl(value) {
527
+ return canonicalValue(value);
528
+ }
529
+ async function signupControlDigest(document) {
530
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
531
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
532
+ }
533
+
534
+ // src/admission-grants.ts
535
+ var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
536
+ var MAX_GRANTS = 2e3;
537
+ var MAX_NOTE = 2e3;
538
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
539
+ var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
540
+ function normalizeAdmissionEmail(email) {
541
+ return normalizeChapterEmail(email);
542
+ }
543
+ function validateAdmissionGrantDocument(input) {
544
+ const errors = [];
545
+ const root = input && typeof input === "object" ? input : {};
546
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
547
+ const revision = Number(root.revision);
548
+ const generatedAt = Number(root.generatedAt);
549
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
550
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
551
+ const source = root.source && typeof root.source === "object" ? root.source : {};
552
+ const target = root.target && typeof root.target === "object" ? root.target : {};
553
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
554
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
555
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
556
+ const environment = typeof target.environment === "string" ? target.environment : "";
557
+ if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
558
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
559
+ if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
560
+ if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
561
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
562
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
563
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
564
+ const grants = [];
565
+ const seenIds = /* @__PURE__ */ new Set();
566
+ const seenEmails = /* @__PURE__ */ new Set();
567
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
568
+ const grant = raw && typeof raw === "object" ? raw : {};
569
+ const id = typeof grant.id === "string" ? grant.id : "";
570
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
571
+ const normalized = normalizeAdmissionEmail(email);
572
+ const decision = grant.decision;
573
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
574
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
575
+ if (!ID.test(id)) errors.push(`document.grants[${index}].id must be an identifier`);
576
+ if (seenIds.has(id)) errors.push(`document.grants repeats id "${id}"`);
577
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
578
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
579
+ if (decision !== "waive_interview" && decision !== "require_interview") {
580
+ errors.push(`document.grants[${index}].decision is invalid`);
581
+ }
582
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
583
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
584
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
585
+ }
586
+ seenIds.add(id);
587
+ seenEmails.add(normalized);
588
+ grants.push({
589
+ id,
590
+ email,
591
+ decision,
592
+ ...note ? { note } : {},
593
+ ...expiresAt !== void 0 ? { expiresAt } : {}
594
+ });
595
+ }
596
+ if (errors.length) return { ok: false, errors };
597
+ return {
598
+ ok: true,
599
+ value: {
600
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
601
+ revision,
602
+ generatedAt,
603
+ source: { appId: sourceId, actor: sourceActor },
604
+ target: { chapterId, environment },
605
+ grants
606
+ }
607
+ };
608
+ }
609
+ var rows = (value) => Array.isArray(value) ? value : [];
610
+ function asGrant(row) {
611
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
612
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
613
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
614
+ return row;
615
+ }
616
+ async function findAdmissionGrant(db, targetId, email, now) {
617
+ const emailNormalized = normalizeAdmissionEmail(email);
618
+ if (!isAdmissionEmail(emailNormalized)) return null;
619
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
620
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
621
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
622
+ )).sort((a, b) => {
623
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
624
+ return scope || b.createdAt - a.createdAt;
625
+ });
626
+ return active[0] ?? null;
627
+ }
628
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
629
+ const where = { sourceId, targetId, environment };
630
+ if (db.aggregate) {
631
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
632
+ }
633
+ const found = await db.query({ admissionGrantRevisions: {} });
634
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
635
+ }
636
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
637
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
638
+ const result = await db.query({ admissionGrantRevisions: { $: {
639
+ where: { sourceId, targetId, environment },
640
+ order: { revision: "desc" },
641
+ limit: 1
642
+ } } });
643
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
644
+ return row ? row : null;
645
+ }
646
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
647
+ const { document, digest } = envelope;
648
+ const current = rows((await db.query({ admissionGrants: { $: {
649
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
650
+ limit: MAX_GRANTS
651
+ } } })).admissionGrants);
652
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
653
+ const ops = [];
654
+ for (const row of current) {
655
+ const id = String(row.id ?? "");
656
+ if (id && !incomingIds.has(id)) ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
657
+ active: false,
658
+ revokedAt: receivedAt,
659
+ revokedBy: document.source.actor,
660
+ updatedAt: receivedAt,
661
+ updatedBy: document.source.actor
662
+ } });
663
+ }
664
+ for (const grant of document.grants) {
665
+ const id = `admission:parent:${document.source.appId}:${grant.id}`;
666
+ const existing = current.find((row) => row.id === id);
667
+ ops.push({ t: "update", ns: "admissionGrants", id, attrs: {
668
+ id,
669
+ sourceGrantId: grant.id,
670
+ sourceScope: "parent",
671
+ sourceId: document.source.appId,
672
+ targetId: document.target.chapterId,
673
+ email: grant.email,
674
+ emailNormalized: normalizeAdmissionEmail(grant.email),
675
+ decision: grant.decision,
676
+ ...grant.note ? { note: grant.note } : {},
677
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
678
+ active: true,
679
+ sourceRevision: document.revision,
680
+ createdAt: Number(existing?.createdAt ?? receivedAt),
681
+ createdBy: String(existing?.createdBy ?? document.source.actor),
682
+ updatedAt: receivedAt,
683
+ updatedBy: document.source.actor
684
+ } });
685
+ const retract = [
686
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
687
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
688
+ ];
689
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id, attrs: retract });
690
+ }
691
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
692
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
693
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
694
+ id: revisionId,
695
+ sourceId: document.source.appId,
696
+ targetId: document.target.chapterId,
697
+ environment: document.target.environment,
698
+ revision: document.revision,
699
+ digest,
700
+ active: true,
701
+ receivedAt
702
+ } });
703
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
704
+ }
705
+
706
+ // src/flow-fields.ts
707
+ var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
708
+ var APPLICATION_LABELS = {
709
+ firstName: "First name",
710
+ lastName: "Last name",
711
+ email: "Email",
712
+ referral: "Referral source",
713
+ referralName: "Referral name",
714
+ whoYouAre: "Who you are",
715
+ linkedin: "LinkedIn",
716
+ message: "Intro message",
717
+ phone: "Phone",
718
+ state: "State"
719
+ };
720
+ function humanize(id) {
721
+ const text3 = id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
722
+ return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id;
723
+ }
724
+ function applicationDefs(chapter) {
725
+ try {
726
+ return chapter.crm.type("person").fields;
727
+ } catch {
728
+ return {};
729
+ }
730
+ }
731
+ function applicationType(id, def) {
732
+ if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
733
+ return id === "email" ? "email" : id === "linkedin" ? "url" : "string";
734
+ }
735
+ function applicationFlowFields(chapter, application = chapter.application) {
736
+ const defs = applicationDefs(chapter);
737
+ return [...application.required, ...application.optional].map((id, order) => {
738
+ const def = defs[id];
739
+ const conditions = application.conditions[id];
740
+ const type = applicationType(id, def);
741
+ return {
742
+ id,
743
+ label: def?.label ?? APPLICATION_LABELS[id] ?? humanize(id),
744
+ type,
745
+ required: application.required.includes(id),
746
+ order,
747
+ maxLength: application.maxLen[id] ?? application.defaultMaxLen,
748
+ ...type === "enum" && def?.options ? { options: [...def.options] } : {},
749
+ ...conditions ? { conditions: { ...conditions } } : {},
750
+ ...type === "email" ? { emailIdentity: "literal" } : {}
751
+ };
752
+ });
753
+ }
754
+ function resolveFormationFlowFields(crm, formation) {
755
+ if (!formation.enabled) return [];
756
+ const def = crm.type(formation.type);
757
+ return [...formation.required, ...formation.optional].map((id, order) => {
758
+ const field = def.fields[id];
759
+ return {
760
+ id,
761
+ label: field.label ?? humanize(id),
762
+ type: field.type,
763
+ required: formation.required.includes(id),
764
+ order,
765
+ maxLength: formation.maxLen[id] ?? formation.defaultMaxLen,
766
+ ...field.options ? { options: [...field.options] } : {},
767
+ ...id === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
768
+ };
769
+ });
770
+ }
771
+ function formationFlowFields(chapter, formation = chapter.formation) {
772
+ return resolveFormationFlowFields(chapter.crm, formation);
773
+ }
774
+ function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
775
+ return {
776
+ schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
777
+ flow,
778
+ fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
779
+ };
780
+ }
781
+
701
782
  // src/gift-offers.ts
702
783
  var GIFT_OFFER_CATALOG_VERSION = 1;
703
784
  var MAX_GIFT_OFFERS = 20;
@@ -1117,10 +1198,10 @@ async function syncApplicationToCrm(deps, opts) {
1117
1198
  return recordId;
1118
1199
  }
1119
1200
  async function backfillCrm(deps) {
1120
- const [appsRes, usersRes] = await Promise.all([
1121
- deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
1122
- deps.db.query({ $users: { $: { limit: 1e3 } } })
1123
- ]);
1201
+ const data = await deps.db.query({
1202
+ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
1203
+ $users: { $: { limit: 1e3 } }
1204
+ });
1124
1205
  const seen = /* @__PURE__ */ new Set();
1125
1206
  let synced = 0;
1126
1207
  const errors = [];
@@ -1135,8 +1216,8 @@ async function backfillCrm(deps) {
1135
1216
  errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
1136
1217
  }
1137
1218
  };
1138
- for (const a of appsRes.applications ?? []) await run(a, str(a.status));
1139
- for (const u of usersRes.$users ?? []) {
1219
+ for (const a of data.applications ?? []) await run(a, str(a.status));
1220
+ for (const u of data.$users ?? []) {
1140
1221
  if (u.deleted === true) continue;
1141
1222
  await run({ email: u.email, firstName: str(u.name) });
1142
1223
  }
@@ -1708,7 +1789,7 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
1708
1789
  // src/worker-routes.ts
1709
1790
  async function memberSessionApplication(db, chapterId, email) {
1710
1791
  let apps = (await db.query({ applications: { $: {
1711
- where: { emailNormalized: normalizeAdmissionEmail(email) },
1792
+ where: { emailNormalized: normalizeChapterEmail(email) },
1712
1793
  limit: 20
1713
1794
  } } })).applications;
1714
1795
  if (!apps?.length) apps = (await db.query({ applications: { $: { where: { email }, limit: 20 } } })).applications;
@@ -1807,6 +1888,7 @@ var handleMember = async (req, url, env, ctx) => {
1807
1888
  const application = controlledApplication(chapter.application, activeControl?.document);
1808
1889
  return json({
1809
1890
  ...joinConfig(group, paymentsReady, joinConfigTiers(offered), application.conditions),
1891
+ fieldContract: chapterFlowFieldContract(chapter, "join", application),
1810
1892
  copy: { ...chapter.copy.join, ...activeControl?.document.application?.copy ?? {} },
1811
1893
  ...activeControl ? { signupControl: {
1812
1894
  revision: activeControl.revision,
@@ -3050,7 +3132,7 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
3050
3132
  return json({ error: "invalid JSON body" }, 400);
3051
3133
  }
3052
3134
  const email = typeof body.email === "string" ? body.email.trim() : "";
3053
- const emailNormalized = normalizeAdmissionEmail(email);
3135
+ const emailNormalized = normalizeChapterEmail(email);
3054
3136
  if (!isValidEmail(emailNormalized)) return json({ error: "email must be valid" }, 400);
3055
3137
  const decision = body.decision ?? "waive_interview";
3056
3138
  if (decision !== "waive_interview" && decision !== "require_interview") {
@@ -3552,7 +3634,7 @@ async function rotateGiftClaim(db, claim, now) {
3552
3634
  async function redeemGiftClaim(db, claim, beneficiary, now) {
3553
3635
  if (claim.status !== "pending" || Number(claim.codeExpiresAt) <= now) return null;
3554
3636
  const chapterId = String(claim.chapterId);
3555
- const emailNormalized = normalizeAdmissionEmail(beneficiary.email);
3637
+ const emailNormalized = normalizeChapterEmail(beneficiary.email);
3556
3638
  const target = claim.target;
3557
3639
  const ops = [];
3558
3640
  let result;
@@ -3849,7 +3931,7 @@ async function startSubscription(req, env, ctx) {
3849
3931
  offer,
3850
3932
  purchaserType: "application",
3851
3933
  purchaserEmail: String(app.email ?? ""),
3852
- purchaserEmailNormalized: normalizeAdmissionEmail(String(app.email ?? "")),
3934
+ purchaserEmailNormalized: normalizeChapterEmail(String(app.email ?? "")),
3853
3935
  purchaserApplicationId: applicationId,
3854
3936
  stripeSecret: secretKey,
3855
3937
  publishableKey: group.stripePublishableKey,
@@ -3998,7 +4080,7 @@ var handleGifts = async (req, url, env, ctx) => {
3998
4080
  if (!user) return json({ error: "unauthorized" }, 401);
3999
4081
  const control2 = await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV);
4000
4082
  const found = await db.query({ giftClaims: { $: { where: {
4001
- purchaserEmailNormalized: normalizeAdmissionEmail(user.email),
4083
+ purchaserEmailNormalized: normalizeChapterEmail(user.email),
4002
4084
  chapterId: ctx.chapter.id
4003
4085
  }, limit: 200 } } });
4004
4086
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
@@ -4025,7 +4107,7 @@ var handleGifts = async (req, url, env, ctx) => {
4025
4107
  const group = first3((await db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })).groups);
4026
4108
  const stripeSecret = await getVaultSecret(db, "stripe_secret_key");
4027
4109
  if (!group || !stripeSecret || typeof group.stripePublishableKey !== "string") return json({ error: "payments not configured" }, 503);
4028
- const emailNormalized = normalizeAdmissionEmail(user.email);
4110
+ const emailNormalized = normalizeChapterEmail(user.email);
4029
4111
  const applications = rows2((await db.query({ applications: { $: { where: { emailNormalized }, limit: 20 } } })).applications);
4030
4112
  const app = applications.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
4031
4113
  try {
@@ -4067,7 +4149,7 @@ var handleGifts = async (req, url, env, ctx) => {
4067
4149
  return invalid();
4068
4150
  }
4069
4151
  }
4070
- const owns = user ? claim.purchaserEmailNormalized === normalizeAdmissionEmail(user.email) : claim.purchaserApplicationId === applicationId;
4152
+ const owns = user ? claim.purchaserEmailNormalized === normalizeChapterEmail(user.email) : claim.purchaserApplicationId === applicationId;
4071
4153
  if (!owns) return invalid();
4072
4154
  const rotated = await rotateGiftClaim(db, claim, Date.now());
4073
4155
  return rotated ? json(rotated) : invalid();
@@ -4260,14 +4342,16 @@ var handleAdminPeople = async (req, url, env, ctx) => {
4260
4342
  if (gate9 instanceof Response) return gate9;
4261
4343
  const { db } = gate9;
4262
4344
  const sk = await getVaultSecret(db, "clerk_secret_key");
4263
- const [appsRes, usersRes, roleList] = await Promise.all([
4264
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
4265
- db.query({ $users: { $: { limit: 200 } } }),
4345
+ const [data, roleList] = await Promise.all([
4346
+ db.query({
4347
+ applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
4348
+ $users: { $: { limit: 200 } }
4349
+ }),
4266
4350
  sk ? clerkListUsers(sk).catch(() => []) : Promise.resolve([])
4267
4351
  ]);
4268
4352
  const clerkByUserId = new Map(roleList.map((u) => [u.id, u]));
4269
4353
  const people = /* @__PURE__ */ new Map();
4270
- for (const u of usersRes.$users ?? []) {
4354
+ for (const u of data.$users ?? []) {
4271
4355
  if (u.deleted === true) continue;
4272
4356
  const email = typeof u.email === "string" ? u.email : "";
4273
4357
  if (!email) continue;
@@ -4282,7 +4366,7 @@ var handleAdminPeople = async (req, url, env, ctx) => {
4282
4366
  application: null
4283
4367
  });
4284
4368
  }
4285
- for (const a of appsRes.applications ?? []) {
4369
+ for (const a of data.applications ?? []) {
4286
4370
  const key = String(a.email ?? "").toLowerCase();
4287
4371
  if (!key) continue;
4288
4372
  const name = `${a.firstName ?? ""} ${a.lastName ?? ""}`.trim();
@@ -4487,13 +4571,19 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4487
4571
  const now = Date.now();
4488
4572
  const d7 = now - 7 * 864e5;
4489
4573
  const d30 = now - 30 * 864e5;
4490
- const [appsRes, meetingsRes, recsRes, groupRes] = await Promise.all([
4491
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
4492
- db.query({ meetings: { $: { where: { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } } }),
4493
- db.query({ crm_record: { $: { where: { type: "person" }, limit: 1e3 } } }),
4494
- db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })
4495
- ]);
4496
- const apps = rows3(appsRes.applications);
4574
+ const data = await db.query({
4575
+ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
4576
+ meetings: {
4577
+ $: {
4578
+ where: { status: "scheduled" },
4579
+ order: { startAt: "asc" },
4580
+ limit: 500
4581
+ }
4582
+ },
4583
+ crm_record: { $: { where: { type: "person" }, limit: 1e3 } },
4584
+ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
4585
+ });
4586
+ const apps = rows3(data.applications);
4497
4587
  const applications = {
4498
4588
  total: apps.length,
4499
4589
  last7: apps.filter((a) => a.createdAt >= d7).length,
@@ -4505,7 +4595,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4505
4595
  pipeline[s] = 0;
4506
4596
  pipelineDelta[s] = 0;
4507
4597
  }
4508
- for (const r of rows3(recsRes.crm_record)) {
4598
+ for (const r of rows3(data.crm_record)) {
4509
4599
  const s = r.stage;
4510
4600
  if (s in pipeline) {
4511
4601
  pipeline[s] = (pipeline[s] ?? 0) + 1;
@@ -4513,7 +4603,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4513
4603
  if (Number.isFinite(sc) && sc >= d7) pipelineDelta[s] = (pipelineDelta[s] ?? 0) + 1;
4514
4604
  }
4515
4605
  }
4516
- const meetings = rows3(meetingsRes.meetings);
4606
+ const meetings = rows3(data.meetings);
4517
4607
  const appById = new Map(apps.map((a) => [a.id, a]));
4518
4608
  const upcoming = meetings.filter((m) => m.startAt >= now - 36e5);
4519
4609
  const calls = { upcoming: upcoming.length, needsAttention: meetings.filter((m) => m.drift && m.drift !== "none").length };
@@ -4529,7 +4619,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4529
4619
  email: a?.email ?? null
4530
4620
  };
4531
4621
  });
4532
- const group = groupRes.groups?.[0];
4622
+ const group = data.groups?.[0];
4533
4623
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
4534
4624
  let revenue = { billingReady: false };
4535
4625
  let revenueSeries = null;
@@ -4563,16 +4653,18 @@ var handleAdminBilling = async (req, url, env, ctx) => {
4563
4653
  const db = got;
4564
4654
  const sk = await getVaultSecret(db, "stripe_secret_key");
4565
4655
  if (!sk) return json({ billingReady: false, rows: [], summary: null });
4566
- const [appsRes, subsRes, groupRes] = await Promise.all([
4567
- db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 200 } } }),
4568
- stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" }),
4569
- db.query({ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } } })
4656
+ const [data, subsRes] = await Promise.all([
4657
+ db.query({
4658
+ applications: { $: { order: { createdAt: "desc" }, limit: 200 } },
4659
+ groups: { $: { where: { id: ctx.chapter.id }, limit: 1 } }
4660
+ }),
4661
+ stripeCall(sk, "GET", "/v1/subscriptions", { limit: 100, status: "all" })
4570
4662
  ]);
4571
- const testMode = String(groupRes.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
4663
+ const testMode = String(data.groups?.[0]?.stripePublishableKey ?? "").startsWith("pk_test");
4572
4664
  if (!subsRes.ok) return json({ error: "billing lookup failed upstream" }, 502);
4573
4665
  const subById = new Map(rows3(subsRes.body.data).map((s) => [s.id, s]));
4574
4666
  const billingRows = [];
4575
- for (const a of rows3(appsRes.applications)) {
4667
+ for (const a of rows3(data.applications)) {
4576
4668
  if (!a.stripeCustomerId && !a.stripeSubscriptionId) continue;
4577
4669
  const sub = a.stripeSubscriptionId ? subById.get(a.stripeSubscriptionId) : void 0;
4578
4670
  const items = sub?.items?.data ?? [];
@@ -4912,21 +5004,27 @@ var handleAdminComms = async (req, url, env, ctx) => {
4912
5004
  if (got instanceof Response) return got;
4913
5005
  const db = got;
4914
5006
  const appId = m[1];
4915
- const [emailRes, meetingRes, appRes] = await Promise.all([
4916
- db.query({ emailLog: { $: { where: { applicationId: appId }, order: { sentAt: "desc" }, limit: 100 } } }),
4917
- db.query({ meetings: { $: { where: { applicationId: appId } } } }),
4918
- db.query({ applications: { $: { where: { id: appId }, limit: 1 } } })
4919
- ]);
4920
- const app = appRes.applications?.[0] ?? null;
5007
+ const data = await db.query({
5008
+ emailLog: {
5009
+ $: {
5010
+ where: { applicationId: appId },
5011
+ order: { sentAt: "desc" },
5012
+ limit: 100
5013
+ }
5014
+ },
5015
+ meetings: { $: { where: { applicationId: appId } } },
5016
+ applications: { $: { where: { id: appId }, limit: 1 } }
5017
+ });
5018
+ const app = data.applications?.[0] ?? null;
4921
5019
  const group = await loadGroup2(db, str2(app?.groupId, ctx.chapter.id));
4922
5020
  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;
4923
- const emails = (emailRes.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
5021
+ const emails = (data.emailLog ?? []).filter((r) => r.template !== "adminNotification").map((r) => {
4924
5022
  let mailBody = typeof r.body === "string" ? r.body : null;
4925
5023
  if (!mailBody && group && vars) mailBody = renderTemplateBody(emailGroupFrom(group), String(r.template), vars);
4926
5024
  const channel = r.error ? "email (failed)" : r.redirected ? "email (dev-redirected)" : r.transport === "log-only" ? "email (not delivered)" : "email";
4927
5025
  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 };
4928
5026
  });
4929
- const calendar = (meetingRes.meetings ?? []).map((m2) => ({
5027
+ const calendar = (data.meetings ?? []).map((m2) => ({
4930
5028
  kind: "calendar",
4931
5029
  channel: "Google Calendar",
4932
5030
  label: m2.status === "cancelled" ? "Invitation (call later cancelled)" : "Meeting invitation",
@@ -5595,31 +5693,17 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5595
5693
 
5596
5694
  // src/worker-routes-formation.ts
5597
5695
  import { createRecord as createRecord3 } from "@odla-ai/crm";
5598
-
5599
- // src/formation.ts
5600
- function formationFields(crm, formation) {
5601
- const def = crm.type(formation.type);
5602
- return [...formation.required, ...formation.optional].map((id) => {
5603
- const field = def.fields[id];
5604
- return {
5605
- id,
5606
- label: field.label ?? id,
5607
- type: field.type,
5608
- required: formation.required.includes(id),
5609
- ...field.options ? { options: field.options } : {}
5610
- };
5611
- });
5612
- }
5613
-
5614
- // src/worker-routes-formation.ts
5615
5696
  var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
5616
5697
  var handleFormation = async (req, url, env, ctx) => {
5617
5698
  const formation = ctx.chapter.formation;
5618
5699
  if (!formation.enabled) return null;
5619
5700
  if (req.method === "GET" && url.pathname === "/api/formation/config") {
5701
+ const fieldContract = chapterFlowFieldContract(ctx.chapter, "formation");
5620
5702
  return json({
5621
5703
  type: formation.type,
5622
- fields: formationFields(ctx.chapter.crm, formation)
5704
+ fieldContract,
5705
+ // Backward compatibility for hosts that consumed the original array.
5706
+ fields: fieldContract.fields
5623
5707
  });
5624
5708
  }
5625
5709
  if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;