@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/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/
|
|
19
|
-
|
|
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
|
|
74
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
277
75
|
function isValidEmail(value) {
|
|
278
|
-
return typeof value === "string" &&
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 && !
|
|
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 && !
|
|
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 && !
|
|
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
|
|
2333
|
-
function
|
|
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(
|
|
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)}:${
|
|
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
|
|
2211
|
+
return canonicalValue(value);
|
|
2348
2212
|
}
|
|
2349
2213
|
async function memberContentDigest(document) {
|
|
2350
|
-
const digest = await crypto.subtle.digest("SHA-256",
|
|
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";
|
|
@@ -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,
|