@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.
package/dist/index.js CHANGED
@@ -15,212 +15,10 @@ async function applicationIdForSubmission(submissionId) {
15
15
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
16
16
  }
17
17
 
18
- // src/admission-grants-contract.ts
19
- var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
20
- var ADMISSION_GRANTS_PATH = "/api/admission-grants";
21
-
22
- // src/signup-control-canonical.ts
23
- var encoder = new TextEncoder();
24
- function canonicalValue(value) {
25
- if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
26
- return JSON.stringify(value);
27
- }
28
- if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
29
- if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
30
- const record3 = value;
31
- const entries = Object.keys(record3).sort().flatMap((key) => {
32
- const item = record3[key];
33
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
34
- });
35
- return `{${entries.join(",")}}`;
36
- }
37
- function canonicalSignupControl(value) {
38
- return canonicalValue(value);
39
- }
40
- async function signupControlDigest(document) {
41
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
42
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
43
- }
44
- async function createSignupControlEnvelope(document) {
45
- return { document, digest: await signupControlDigest(document) };
46
- }
47
-
48
- // src/admission-grants.ts
49
- var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
50
- var MAX_GRANTS = 2e3;
51
- var MAX_NOTE = 2e3;
52
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
53
- var isAdmissionEmail = (value) => EMAIL_RE.test(value);
54
- function normalizeAdmissionEmail(email) {
18
+ // src/email-identity.ts
19
+ function normalizeChapterEmail(email) {
55
20
  return email.trim().toLowerCase();
56
21
  }
57
- function validateAdmissionGrantDocument(input) {
58
- const errors = [];
59
- const root = input && typeof input === "object" ? input : {};
60
- if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
61
- const revision = Number(root.revision);
62
- const generatedAt = Number(root.generatedAt);
63
- if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
64
- if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
65
- const source = root.source && typeof root.source === "object" ? root.source : {};
66
- const target = root.target && typeof root.target === "object" ? root.target : {};
67
- const sourceId = typeof source.appId === "string" ? source.appId : "";
68
- const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
69
- const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
70
- const environment = typeof target.environment === "string" ? target.environment : "";
71
- if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
72
- if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
73
- if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
74
- if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
75
- if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
76
- const rawGrants = Array.isArray(root.grants) ? root.grants : [];
77
- if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
78
- const grants = [];
79
- const seenIds = /* @__PURE__ */ new Set();
80
- const seenEmails = /* @__PURE__ */ new Set();
81
- for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
82
- const grant = raw && typeof raw === "object" ? raw : {};
83
- const id4 = typeof grant.id === "string" ? grant.id : "";
84
- const email = typeof grant.email === "string" ? grant.email.trim() : "";
85
- const normalized = normalizeAdmissionEmail(email);
86
- const decision = grant.decision;
87
- const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
88
- const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
89
- if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
90
- if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
91
- if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
92
- if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
93
- if (decision !== "waive_interview" && decision !== "require_interview") {
94
- errors.push(`document.grants[${index}].decision is invalid`);
95
- }
96
- if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
97
- if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
98
- errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
99
- }
100
- seenIds.add(id4);
101
- seenEmails.add(normalized);
102
- grants.push({
103
- id: id4,
104
- email,
105
- decision,
106
- ...note ? { note } : {},
107
- ...expiresAt !== void 0 ? { expiresAt } : {}
108
- });
109
- }
110
- if (errors.length) return { ok: false, errors };
111
- return {
112
- ok: true,
113
- value: {
114
- schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
115
- revision,
116
- generatedAt,
117
- source: { appId: sourceId, actor: sourceActor },
118
- target: { chapterId, environment },
119
- grants
120
- }
121
- };
122
- }
123
- async function createAdmissionGrantEnvelope(document) {
124
- const checked = validateAdmissionGrantDocument(document);
125
- if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
126
- return { document: checked.value, digest: await signupControlDigest(checked.value) };
127
- }
128
- var rows = (value) => Array.isArray(value) ? value : [];
129
- function asGrant(row) {
130
- if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
131
- if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
132
- if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
133
- return row;
134
- }
135
- async function findAdmissionGrant(db, targetId, email, now) {
136
- const emailNormalized = normalizeAdmissionEmail(email);
137
- if (!isAdmissionEmail(emailNormalized)) return null;
138
- const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
139
- const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
140
- grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
141
- )).sort((a, b) => {
142
- const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
143
- return scope || b.createdAt - a.createdAt;
144
- });
145
- return active[0] ?? null;
146
- }
147
- async function hasAdmissionRevision(db, sourceId, targetId, environment) {
148
- const where = { sourceId, targetId, environment };
149
- if (db.aggregate) {
150
- return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
151
- }
152
- const found = await db.query({ admissionGrantRevisions: {} });
153
- return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
154
- }
155
- async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
156
- if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
157
- const result = await db.query({ admissionGrantRevisions: { $: {
158
- where: { sourceId, targetId, environment },
159
- order: { revision: "desc" },
160
- limit: 1
161
- } } });
162
- const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
163
- return row ? row : null;
164
- }
165
- async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
166
- const { document, digest } = envelope;
167
- const current = rows((await db.query({ admissionGrants: { $: {
168
- where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
169
- limit: MAX_GRANTS
170
- } } })).admissionGrants);
171
- const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
172
- const ops = [];
173
- for (const row of current) {
174
- const id4 = String(row.id ?? "");
175
- if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
176
- active: false,
177
- revokedAt: receivedAt,
178
- revokedBy: document.source.actor,
179
- updatedAt: receivedAt,
180
- updatedBy: document.source.actor
181
- } });
182
- }
183
- for (const grant of document.grants) {
184
- const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
185
- const existing = current.find((row) => row.id === id4);
186
- ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
187
- id: id4,
188
- sourceGrantId: grant.id,
189
- sourceScope: "parent",
190
- sourceId: document.source.appId,
191
- targetId: document.target.chapterId,
192
- email: grant.email,
193
- emailNormalized: normalizeAdmissionEmail(grant.email),
194
- decision: grant.decision,
195
- ...grant.note ? { note: grant.note } : {},
196
- ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
197
- active: true,
198
- sourceRevision: document.revision,
199
- createdAt: Number(existing?.createdAt ?? receivedAt),
200
- createdBy: String(existing?.createdBy ?? document.source.actor),
201
- updatedAt: receivedAt,
202
- updatedBy: document.source.actor
203
- } });
204
- const retract = [
205
- ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
206
- ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
207
- ];
208
- if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
209
- }
210
- if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
211
- const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
212
- ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
213
- id: revisionId,
214
- sourceId: document.source.appId,
215
- targetId: document.target.chapterId,
216
- environment: document.target.environment,
217
- revision: document.revision,
218
- digest,
219
- active: true,
220
- receivedAt
221
- } });
222
- await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
223
- }
224
22
 
225
23
  // src/member.ts
226
24
  var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
@@ -273,9 +71,9 @@ function resolveApplication(a) {
273
71
  validateEmail: a?.validateEmail ?? true
274
72
  };
275
73
  }
276
- var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
74
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
277
75
  function isValidEmail(value) {
278
- return typeof value === "string" && EMAIL_RE2.test(value);
76
+ return typeof value === "string" && EMAIL_RE.test(value);
279
77
  }
280
78
  function clampArray(value, max) {
281
79
  if (!Array.isArray(value)) return value;
@@ -310,7 +108,7 @@ async function submitApplication(db, chapter, fields, opts) {
310
108
  const cap = app.maxLen[f] ?? app.defaultMaxLen;
311
109
  if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
312
110
  }
313
- if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
111
+ if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
314
112
  return { ok: false, error: "email must be a valid email address" };
315
113
  }
316
114
  const acked = hasDisclaimerAck(fields);
@@ -326,7 +124,7 @@ async function submitApplication(db, chapter, fields, opts) {
326
124
  if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
327
125
  if (opts.groupId) row.groupId = opts.groupId;
328
126
  if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
329
- row.emailNormalized = normalizeAdmissionEmail(fields.email);
127
+ row.emailNormalized = normalizeChapterEmail(fields.email);
330
128
  }
331
129
  if (opts.admissionGrant?.decision === "waive_interview") {
332
130
  row.admissionGrantId = opts.admissionGrant.id;
@@ -1388,6 +1186,82 @@ function formatChapterCopy(template, values) {
1388
1186
  );
1389
1187
  }
1390
1188
 
1189
+ // src/flow-fields.ts
1190
+ var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
1191
+ var APPLICATION_LABELS = {
1192
+ firstName: "First name",
1193
+ lastName: "Last name",
1194
+ email: "Email",
1195
+ referral: "Referral source",
1196
+ referralName: "Referral name",
1197
+ whoYouAre: "Who you are",
1198
+ linkedin: "LinkedIn",
1199
+ message: "Intro message",
1200
+ phone: "Phone",
1201
+ state: "State"
1202
+ };
1203
+ function humanize(id4) {
1204
+ const text3 = id4.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
1205
+ return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id4;
1206
+ }
1207
+ function applicationDefs(chapter) {
1208
+ try {
1209
+ return chapter.crm.type("person").fields;
1210
+ } catch {
1211
+ return {};
1212
+ }
1213
+ }
1214
+ function applicationType(id4, def) {
1215
+ if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
1216
+ return id4 === "email" ? "email" : id4 === "linkedin" ? "url" : "string";
1217
+ }
1218
+ function applicationFlowFields(chapter, application = chapter.application) {
1219
+ const defs = applicationDefs(chapter);
1220
+ return [...application.required, ...application.optional].map((id4, order) => {
1221
+ const def = defs[id4];
1222
+ const conditions = application.conditions[id4];
1223
+ const type = applicationType(id4, def);
1224
+ return {
1225
+ id: id4,
1226
+ label: def?.label ?? APPLICATION_LABELS[id4] ?? humanize(id4),
1227
+ type,
1228
+ required: application.required.includes(id4),
1229
+ order,
1230
+ maxLength: application.maxLen[id4] ?? application.defaultMaxLen,
1231
+ ...type === "enum" && def?.options ? { options: [...def.options] } : {},
1232
+ ...conditions ? { conditions: { ...conditions } } : {},
1233
+ ...type === "email" ? { emailIdentity: "literal" } : {}
1234
+ };
1235
+ });
1236
+ }
1237
+ function resolveFormationFlowFields(crm, formation) {
1238
+ if (!formation.enabled) return [];
1239
+ const def = crm.type(formation.type);
1240
+ return [...formation.required, ...formation.optional].map((id4, order) => {
1241
+ const field = def.fields[id4];
1242
+ return {
1243
+ id: id4,
1244
+ label: field.label ?? humanize(id4),
1245
+ type: field.type,
1246
+ required: formation.required.includes(id4),
1247
+ order,
1248
+ maxLength: formation.maxLen[id4] ?? formation.defaultMaxLen,
1249
+ ...field.options ? { options: [...field.options] } : {},
1250
+ ...id4 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
1251
+ };
1252
+ });
1253
+ }
1254
+ function formationFlowFields(chapter, formation = chapter.formation) {
1255
+ return resolveFormationFlowFields(chapter.crm, formation);
1256
+ }
1257
+ function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
1258
+ return {
1259
+ schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
1260
+ flow,
1261
+ fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
1262
+ };
1263
+ }
1264
+
1391
1265
  // src/formation.ts
1392
1266
  var DEFAULT_REQUIRED2 = ["name", "email", "location", "thesis"];
1393
1267
  var DEFAULT_OPTIONAL2 = ["proposedChapter", "experience", "collaborators"];
@@ -1450,17 +1324,7 @@ function resolveLeaderFormation(input, crm) {
1450
1324
  };
1451
1325
  }
1452
1326
  function formationFields(crm, formation) {
1453
- const def = crm.type(formation.type);
1454
- return [...formation.required, ...formation.optional].map((id4) => {
1455
- const field = def.fields[id4];
1456
- return {
1457
- id: id4,
1458
- label: field.label ?? id4,
1459
- type: field.type,
1460
- required: formation.required.includes(id4),
1461
- ...field.options ? { options: field.options } : {}
1462
- };
1463
- });
1327
+ return resolveFormationFlowFields(crm, formation);
1464
1328
  }
1465
1329
 
1466
1330
  // src/config-network.ts
@@ -2039,7 +1903,7 @@ function createChapterIntegration(chapter, options = {}) {
2039
1903
  }
2040
1904
 
2041
1905
  // src/member-content-validation-helpers.ts
2042
- var ID2 = /^[a-z][a-z0-9-]{0,79}$/;
1906
+ var ID = /^[a-z][a-z0-9-]{0,79}$/;
2043
1907
  var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2044
1908
  var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
2045
1909
  var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
@@ -2079,7 +1943,7 @@ function offerValue(value, path, errors, tiers2, audience) {
2079
1943
  }
2080
1944
  unknownKeys(raw, OFFER_KEYS, path, errors);
2081
1945
  const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
2082
- if (tierId && !ID2.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
1946
+ if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
2083
1947
  if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
2084
1948
  if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
2085
1949
  errors.push(`${path}.tierId must grant access to this page`);
@@ -2112,7 +1976,7 @@ function validateMemberContentDocument(input, options = {}) {
2112
1976
  const type = sourceRaw.type;
2113
1977
  const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
2114
1978
  if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
2115
- if (appId && !ID2.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
1979
+ if (appId && !ID.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
2116
1980
  if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
2117
1981
  if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
2118
1982
  if (type === "parent" || type === "chapter") source = { type, appId };
@@ -2149,7 +2013,7 @@ function validateMemberContentDocument(input, options = {}) {
2149
2013
  const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
2150
2014
  const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
2151
2015
  const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
2152
- if (id4 && !ID2.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2016
+ if (id4 && !ID.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
2153
2017
  if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
2154
2018
  if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
2155
2019
  if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
@@ -2329,25 +2193,25 @@ function projectMemberContent(content, viewer, tiers2) {
2329
2193
  }
2330
2194
 
2331
2195
  // src/member-content-canonical.ts
2332
- var encoder2 = new TextEncoder();
2333
- function canonicalValue2(value) {
2196
+ var encoder = new TextEncoder();
2197
+ function canonicalValue(value) {
2334
2198
  if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2335
2199
  return JSON.stringify(value);
2336
2200
  }
2337
- if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
2201
+ if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
2338
2202
  if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
2339
2203
  const record3 = value;
2340
2204
  const entries = Object.keys(record3).sort().flatMap((key) => {
2341
2205
  const item = record3[key];
2342
- return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
2206
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
2343
2207
  });
2344
2208
  return `{${entries.join(",")}}`;
2345
2209
  }
2346
2210
  function canonicalMemberContent(value) {
2347
- return canonicalValue2(value);
2211
+ return canonicalValue(value);
2348
2212
  }
2349
2213
  async function memberContentDigest(document) {
2350
- const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalMemberContent(document)));
2214
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalMemberContent(document)));
2351
2215
  return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2352
2216
  }
2353
2217
  async function createMemberContentEnvelope(document) {
@@ -2527,6 +2391,213 @@ function canceledPatch() {
2527
2391
  return { canceled: true };
2528
2392
  }
2529
2393
 
2394
+ // src/admission-grants-contract.ts
2395
+ var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
2396
+ var ADMISSION_GRANTS_PATH = "/api/admission-grants";
2397
+
2398
+ // src/signup-control-canonical.ts
2399
+ var encoder2 = new TextEncoder();
2400
+ function canonicalValue2(value) {
2401
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
2402
+ return JSON.stringify(value);
2403
+ }
2404
+ if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
2405
+ if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
2406
+ const record3 = value;
2407
+ const entries = Object.keys(record3).sort().flatMap((key) => {
2408
+ const item = record3[key];
2409
+ return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
2410
+ });
2411
+ return `{${entries.join(",")}}`;
2412
+ }
2413
+ function canonicalSignupControl(value) {
2414
+ return canonicalValue2(value);
2415
+ }
2416
+ async function signupControlDigest(document) {
2417
+ const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalSignupControl(document)));
2418
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
2419
+ }
2420
+ async function createSignupControlEnvelope(document) {
2421
+ return { document, digest: await signupControlDigest(document) };
2422
+ }
2423
+
2424
+ // src/admission-grants.ts
2425
+ var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
2426
+ var MAX_GRANTS = 2e3;
2427
+ var MAX_NOTE = 2e3;
2428
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2429
+ var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
2430
+ function normalizeAdmissionEmail(email) {
2431
+ return normalizeChapterEmail(email);
2432
+ }
2433
+ function validateAdmissionGrantDocument(input) {
2434
+ const errors = [];
2435
+ const root = input && typeof input === "object" ? input : {};
2436
+ if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
2437
+ const revision = Number(root.revision);
2438
+ const generatedAt = Number(root.generatedAt);
2439
+ if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
2440
+ if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
2441
+ const source = root.source && typeof root.source === "object" ? root.source : {};
2442
+ const target = root.target && typeof root.target === "object" ? root.target : {};
2443
+ const sourceId = typeof source.appId === "string" ? source.appId : "";
2444
+ const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
2445
+ const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
2446
+ const environment = typeof target.environment === "string" ? target.environment : "";
2447
+ if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
2448
+ if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
2449
+ if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
2450
+ if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
2451
+ if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
2452
+ const rawGrants = Array.isArray(root.grants) ? root.grants : [];
2453
+ if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
2454
+ const grants = [];
2455
+ const seenIds = /* @__PURE__ */ new Set();
2456
+ const seenEmails = /* @__PURE__ */ new Set();
2457
+ for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
2458
+ const grant = raw && typeof raw === "object" ? raw : {};
2459
+ const id4 = typeof grant.id === "string" ? grant.id : "";
2460
+ const email = typeof grant.email === "string" ? grant.email.trim() : "";
2461
+ const normalized = normalizeAdmissionEmail(email);
2462
+ const decision = grant.decision;
2463
+ const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
2464
+ const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
2465
+ if (!ID2.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
2466
+ if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
2467
+ if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
2468
+ if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
2469
+ if (decision !== "waive_interview" && decision !== "require_interview") {
2470
+ errors.push(`document.grants[${index}].decision is invalid`);
2471
+ }
2472
+ if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
2473
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
2474
+ errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
2475
+ }
2476
+ seenIds.add(id4);
2477
+ seenEmails.add(normalized);
2478
+ grants.push({
2479
+ id: id4,
2480
+ email,
2481
+ decision,
2482
+ ...note ? { note } : {},
2483
+ ...expiresAt !== void 0 ? { expiresAt } : {}
2484
+ });
2485
+ }
2486
+ if (errors.length) return { ok: false, errors };
2487
+ return {
2488
+ ok: true,
2489
+ value: {
2490
+ schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
2491
+ revision,
2492
+ generatedAt,
2493
+ source: { appId: sourceId, actor: sourceActor },
2494
+ target: { chapterId, environment },
2495
+ grants
2496
+ }
2497
+ };
2498
+ }
2499
+ async function createAdmissionGrantEnvelope(document) {
2500
+ const checked = validateAdmissionGrantDocument(document);
2501
+ if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
2502
+ return { document: checked.value, digest: await signupControlDigest(checked.value) };
2503
+ }
2504
+ var rows = (value) => Array.isArray(value) ? value : [];
2505
+ function asGrant(row) {
2506
+ if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
2507
+ if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
2508
+ if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
2509
+ return row;
2510
+ }
2511
+ async function findAdmissionGrant(db, targetId, email, now) {
2512
+ const emailNormalized = normalizeAdmissionEmail(email);
2513
+ if (!isAdmissionEmail(emailNormalized)) return null;
2514
+ const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
2515
+ const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
2516
+ grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
2517
+ )).sort((a, b) => {
2518
+ const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
2519
+ return scope || b.createdAt - a.createdAt;
2520
+ });
2521
+ return active[0] ?? null;
2522
+ }
2523
+ async function hasAdmissionRevision(db, sourceId, targetId, environment) {
2524
+ const where = { sourceId, targetId, environment };
2525
+ if (db.aggregate) {
2526
+ return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
2527
+ }
2528
+ const found = await db.query({ admissionGrantRevisions: {} });
2529
+ return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
2530
+ }
2531
+ async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
2532
+ if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
2533
+ const result = await db.query({ admissionGrantRevisions: { $: {
2534
+ where: { sourceId, targetId, environment },
2535
+ order: { revision: "desc" },
2536
+ limit: 1
2537
+ } } });
2538
+ const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
2539
+ return row ? row : null;
2540
+ }
2541
+ async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
2542
+ const { document, digest } = envelope;
2543
+ const current = rows((await db.query({ admissionGrants: { $: {
2544
+ where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
2545
+ limit: MAX_GRANTS
2546
+ } } })).admissionGrants);
2547
+ const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
2548
+ const ops = [];
2549
+ for (const row of current) {
2550
+ const id4 = String(row.id ?? "");
2551
+ if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
2552
+ active: false,
2553
+ revokedAt: receivedAt,
2554
+ revokedBy: document.source.actor,
2555
+ updatedAt: receivedAt,
2556
+ updatedBy: document.source.actor
2557
+ } });
2558
+ }
2559
+ for (const grant of document.grants) {
2560
+ const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
2561
+ const existing = current.find((row) => row.id === id4);
2562
+ ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
2563
+ id: id4,
2564
+ sourceGrantId: grant.id,
2565
+ sourceScope: "parent",
2566
+ sourceId: document.source.appId,
2567
+ targetId: document.target.chapterId,
2568
+ email: grant.email,
2569
+ emailNormalized: normalizeAdmissionEmail(grant.email),
2570
+ decision: grant.decision,
2571
+ ...grant.note ? { note: grant.note } : {},
2572
+ ...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
2573
+ active: true,
2574
+ sourceRevision: document.revision,
2575
+ createdAt: Number(existing?.createdAt ?? receivedAt),
2576
+ createdBy: String(existing?.createdBy ?? document.source.actor),
2577
+ updatedAt: receivedAt,
2578
+ updatedBy: document.source.actor
2579
+ } });
2580
+ const retract = [
2581
+ ...!grant.note && existing?.note !== void 0 ? ["note"] : [],
2582
+ ...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
2583
+ ];
2584
+ if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
2585
+ }
2586
+ if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
2587
+ const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
2588
+ ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
2589
+ id: revisionId,
2590
+ sourceId: document.source.appId,
2591
+ targetId: document.target.chapterId,
2592
+ environment: document.target.environment,
2593
+ revision: document.revision,
2594
+ digest,
2595
+ active: true,
2596
+ receivedAt
2597
+ } });
2598
+ await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
2599
+ }
2600
+
2530
2601
  // src/join-continuation.ts
2531
2602
  function resolveJoinContinuation(input) {
2532
2603
  if (input.status === "approved") return "accepted";
@@ -2896,10 +2967,10 @@ async function syncApplicationToCrm(deps, opts) {
2896
2967
  return recordId;
2897
2968
  }
2898
2969
  async function backfillCrm(deps) {
2899
- const [appsRes, usersRes] = await Promise.all([
2900
- deps.db.query({ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } } }),
2901
- deps.db.query({ $users: { $: { limit: 1e3 } } })
2902
- ]);
2970
+ const data = await deps.db.query({
2971
+ applications: { $: { order: { createdAt: "desc" }, limit: 1e3 } },
2972
+ $users: { $: { limit: 1e3 } }
2973
+ });
2903
2974
  const seen = /* @__PURE__ */ new Set();
2904
2975
  let synced = 0;
2905
2976
  const errors = [];
@@ -2914,8 +2985,8 @@ async function backfillCrm(deps) {
2914
2985
  errors.push({ email: key, error: err instanceof Error ? err.message : String(err) });
2915
2986
  }
2916
2987
  };
2917
- for (const a of appsRes.applications ?? []) await run(a, str(a.status));
2918
- for (const u of usersRes.$users ?? []) {
2988
+ for (const a of data.applications ?? []) await run(a, str(a.status));
2989
+ for (const u of data.$users ?? []) {
2919
2990
  if (u.deleted === true) continue;
2920
2991
  await run({ email: u.email, firstName: str(u.name) });
2921
2992
  }
@@ -4192,6 +4263,7 @@ async function exportChapterPromotion(input) {
4192
4263
  export {
4193
4264
  ADMISSION_GRANTS_PATH,
4194
4265
  ADMISSION_GRANTS_SCHEMA_VERSION,
4266
+ CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
4195
4267
  CHAPTER_PROMOTION_ADAPTER,
4196
4268
  DEFAULT_CHAPTER_COPY,
4197
4269
  DEFAULT_SHARE_FIELDS,
@@ -4208,6 +4280,7 @@ export {
4208
4280
  SIGNUP_CONTROL_THEME_STYLE_ID,
4209
4281
  applicantProfile,
4210
4282
  applicationBookingUpdate,
4283
+ applicationFlowFields,
4211
4284
  applicationSummary,
4212
4285
  applyMembershipTierMarkdown,
4213
4286
  backfillCrm,
@@ -4225,6 +4298,7 @@ export {
4225
4298
  canonicalSignupControl,
4226
4299
  chapterBrandFromTokens,
4227
4300
  chapterDb,
4301
+ chapterFlowFieldContract,
4228
4302
  clampArray,
4229
4303
  clerkGetUser,
4230
4304
  clerkGetUserByEmail,
@@ -4253,6 +4327,7 @@ export {
4253
4327
  firstPaymentPatch,
4254
4328
  formatChapterCopy,
4255
4329
  formationFields,
4330
+ formationFlowFields,
4256
4331
  getVaultSecret,
4257
4332
  giftOfferActive,
4258
4333
  hasDisclaimerAck,
@@ -4281,6 +4356,7 @@ export {
4281
4356
  membershipTierPresentationFromMarkdown,
4282
4357
  networkSourceTag,
4283
4358
  normalizeAdmissionEmail,
4359
+ normalizeChapterEmail,
4284
4360
  normalizeSharedRecord,
4285
4361
  normalizeWebhookEvent,
4286
4362
  offerableTiers,