@odla-ai/chapter 0.37.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -2
- package/dist/index.cjs +305 -229
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -8
- package/dist/index.d.ts +55 -8
- package/dist/index.js +305 -229
- package/dist/index.js.map +1 -1
- package/dist/ui/member/index.d.ts +35 -1
- package/dist/worker/index.cjs +293 -225
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.js +293 -225
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.js
CHANGED
|
@@ -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/
|
|
171
|
-
|
|
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
|
|
226
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
421
227
|
function isValidEmail(value) {
|
|
422
|
-
return typeof value === "string" &&
|
|
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 =
|
|
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;
|
|
@@ -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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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 ===
|
|
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();
|
|
@@ -5595,31 +5677,17 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5595
5677
|
|
|
5596
5678
|
// src/worker-routes-formation.ts
|
|
5597
5679
|
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
5680
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
5616
5681
|
var handleFormation = async (req, url, env, ctx) => {
|
|
5617
5682
|
const formation = ctx.chapter.formation;
|
|
5618
5683
|
if (!formation.enabled) return null;
|
|
5619
5684
|
if (req.method === "GET" && url.pathname === "/api/formation/config") {
|
|
5685
|
+
const fieldContract = chapterFlowFieldContract(ctx.chapter, "formation");
|
|
5620
5686
|
return json({
|
|
5621
5687
|
type: formation.type,
|
|
5622
|
-
|
|
5688
|
+
fieldContract,
|
|
5689
|
+
// Backward compatibility for hosts that consumed the original array.
|
|
5690
|
+
fields: fieldContract.fields
|
|
5623
5691
|
});
|
|
5624
5692
|
}
|
|
5625
5693
|
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|