@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.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ADMISSION_GRANTS_PATH: () => ADMISSION_GRANTS_PATH,
|
|
24
24
|
ADMISSION_GRANTS_SCHEMA_VERSION: () => ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
25
|
+
CHAPTER_FLOW_FIELDS_SCHEMA_VERSION: () => CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
25
26
|
CHAPTER_PROMOTION_ADAPTER: () => CHAPTER_PROMOTION_ADAPTER,
|
|
26
27
|
DEFAULT_CHAPTER_COPY: () => DEFAULT_CHAPTER_COPY,
|
|
27
28
|
DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
|
|
@@ -38,6 +39,7 @@ __export(index_exports, {
|
|
|
38
39
|
SIGNUP_CONTROL_THEME_STYLE_ID: () => SIGNUP_CONTROL_THEME_STYLE_ID,
|
|
39
40
|
applicantProfile: () => applicantProfile,
|
|
40
41
|
applicationBookingUpdate: () => applicationBookingUpdate,
|
|
42
|
+
applicationFlowFields: () => applicationFlowFields,
|
|
41
43
|
applicationSummary: () => applicationSummary,
|
|
42
44
|
applyMembershipTierMarkdown: () => applyMembershipTierMarkdown,
|
|
43
45
|
backfillCrm: () => backfillCrm,
|
|
@@ -55,6 +57,7 @@ __export(index_exports, {
|
|
|
55
57
|
canonicalSignupControl: () => canonicalSignupControl,
|
|
56
58
|
chapterBrandFromTokens: () => chapterBrandFromTokens,
|
|
57
59
|
chapterDb: () => chapterDb,
|
|
60
|
+
chapterFlowFieldContract: () => chapterFlowFieldContract,
|
|
58
61
|
clampArray: () => clampArray,
|
|
59
62
|
clerkGetUser: () => clerkGetUser,
|
|
60
63
|
clerkGetUserByEmail: () => clerkGetUserByEmail,
|
|
@@ -83,6 +86,7 @@ __export(index_exports, {
|
|
|
83
86
|
firstPaymentPatch: () => firstPaymentPatch,
|
|
84
87
|
formatChapterCopy: () => formatChapterCopy,
|
|
85
88
|
formationFields: () => formationFields,
|
|
89
|
+
formationFlowFields: () => formationFlowFields,
|
|
86
90
|
getVaultSecret: () => getVaultSecret,
|
|
87
91
|
giftOfferActive: () => giftOfferActive,
|
|
88
92
|
hasDisclaimerAck: () => hasDisclaimerAck,
|
|
@@ -111,6 +115,7 @@ __export(index_exports, {
|
|
|
111
115
|
membershipTierPresentationFromMarkdown: () => membershipTierPresentationFromMarkdown,
|
|
112
116
|
networkSourceTag: () => networkSourceTag,
|
|
113
117
|
normalizeAdmissionEmail: () => normalizeAdmissionEmail,
|
|
118
|
+
normalizeChapterEmail: () => normalizeChapterEmail,
|
|
114
119
|
normalizeSharedRecord: () => normalizeSharedRecord,
|
|
115
120
|
normalizeWebhookEvent: () => normalizeWebhookEvent,
|
|
116
121
|
offerableTiers: () => offerableTiers,
|
|
@@ -183,212 +188,10 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
183
188
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
184
189
|
}
|
|
185
190
|
|
|
186
|
-
// src/
|
|
187
|
-
|
|
188
|
-
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
189
|
-
|
|
190
|
-
// src/signup-control-canonical.ts
|
|
191
|
-
var encoder = new TextEncoder();
|
|
192
|
-
function canonicalValue(value) {
|
|
193
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
194
|
-
return JSON.stringify(value);
|
|
195
|
-
}
|
|
196
|
-
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
197
|
-
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
198
|
-
const record3 = value;
|
|
199
|
-
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
200
|
-
const item = record3[key];
|
|
201
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
202
|
-
});
|
|
203
|
-
return `{${entries.join(",")}}`;
|
|
204
|
-
}
|
|
205
|
-
function canonicalSignupControl(value) {
|
|
206
|
-
return canonicalValue(value);
|
|
207
|
-
}
|
|
208
|
-
async function signupControlDigest(document) {
|
|
209
|
-
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalSignupControl(document)));
|
|
210
|
-
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
211
|
-
}
|
|
212
|
-
async function createSignupControlEnvelope(document) {
|
|
213
|
-
return { document, digest: await signupControlDigest(document) };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// src/admission-grants.ts
|
|
217
|
-
var ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
218
|
-
var MAX_GRANTS = 2e3;
|
|
219
|
-
var MAX_NOTE = 2e3;
|
|
220
|
-
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
221
|
-
var isAdmissionEmail = (value) => EMAIL_RE.test(value);
|
|
222
|
-
function normalizeAdmissionEmail(email) {
|
|
191
|
+
// src/email-identity.ts
|
|
192
|
+
function normalizeChapterEmail(email) {
|
|
223
193
|
return email.trim().toLowerCase();
|
|
224
194
|
}
|
|
225
|
-
function validateAdmissionGrantDocument(input) {
|
|
226
|
-
const errors = [];
|
|
227
|
-
const root = input && typeof input === "object" ? input : {};
|
|
228
|
-
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
229
|
-
const revision = Number(root.revision);
|
|
230
|
-
const generatedAt = Number(root.generatedAt);
|
|
231
|
-
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
232
|
-
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
233
|
-
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
234
|
-
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
235
|
-
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
236
|
-
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
237
|
-
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
238
|
-
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
239
|
-
if (!ID.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
240
|
-
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
241
|
-
if (!ID.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
242
|
-
if (!ID.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
243
|
-
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
244
|
-
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
245
|
-
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
246
|
-
const grants = [];
|
|
247
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
248
|
-
const seenEmails = /* @__PURE__ */ new Set();
|
|
249
|
-
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
250
|
-
const grant = raw && typeof raw === "object" ? raw : {};
|
|
251
|
-
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
252
|
-
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
253
|
-
const normalized = normalizeAdmissionEmail(email);
|
|
254
|
-
const decision = grant.decision;
|
|
255
|
-
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
256
|
-
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
257
|
-
if (!ID.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
258
|
-
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
259
|
-
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
260
|
-
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
261
|
-
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
262
|
-
errors.push(`document.grants[${index}].decision is invalid`);
|
|
263
|
-
}
|
|
264
|
-
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
265
|
-
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
266
|
-
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
267
|
-
}
|
|
268
|
-
seenIds.add(id4);
|
|
269
|
-
seenEmails.add(normalized);
|
|
270
|
-
grants.push({
|
|
271
|
-
id: id4,
|
|
272
|
-
email,
|
|
273
|
-
decision,
|
|
274
|
-
...note ? { note } : {},
|
|
275
|
-
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
if (errors.length) return { ok: false, errors };
|
|
279
|
-
return {
|
|
280
|
-
ok: true,
|
|
281
|
-
value: {
|
|
282
|
-
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
283
|
-
revision,
|
|
284
|
-
generatedAt,
|
|
285
|
-
source: { appId: sourceId, actor: sourceActor },
|
|
286
|
-
target: { chapterId, environment },
|
|
287
|
-
grants
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
async function createAdmissionGrantEnvelope(document) {
|
|
292
|
-
const checked = validateAdmissionGrantDocument(document);
|
|
293
|
-
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
294
|
-
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
295
|
-
}
|
|
296
|
-
var rows = (value) => Array.isArray(value) ? value : [];
|
|
297
|
-
function asGrant(row) {
|
|
298
|
-
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
299
|
-
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
300
|
-
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
301
|
-
return row;
|
|
302
|
-
}
|
|
303
|
-
async function findAdmissionGrant(db, targetId, email, now) {
|
|
304
|
-
const emailNormalized = normalizeAdmissionEmail(email);
|
|
305
|
-
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
306
|
-
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
307
|
-
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
308
|
-
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
309
|
-
)).sort((a, b) => {
|
|
310
|
-
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
311
|
-
return scope || b.createdAt - a.createdAt;
|
|
312
|
-
});
|
|
313
|
-
return active[0] ?? null;
|
|
314
|
-
}
|
|
315
|
-
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
316
|
-
const where = { sourceId, targetId, environment };
|
|
317
|
-
if (db.aggregate) {
|
|
318
|
-
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
319
|
-
}
|
|
320
|
-
const found = await db.query({ admissionGrantRevisions: {} });
|
|
321
|
-
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
322
|
-
}
|
|
323
|
-
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
324
|
-
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
325
|
-
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
326
|
-
where: { sourceId, targetId, environment },
|
|
327
|
-
order: { revision: "desc" },
|
|
328
|
-
limit: 1
|
|
329
|
-
} } });
|
|
330
|
-
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
331
|
-
return row ? row : null;
|
|
332
|
-
}
|
|
333
|
-
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
334
|
-
const { document, digest } = envelope;
|
|
335
|
-
const current = rows((await db.query({ admissionGrants: { $: {
|
|
336
|
-
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
337
|
-
limit: MAX_GRANTS
|
|
338
|
-
} } })).admissionGrants);
|
|
339
|
-
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
340
|
-
const ops = [];
|
|
341
|
-
for (const row of current) {
|
|
342
|
-
const id4 = String(row.id ?? "");
|
|
343
|
-
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
344
|
-
active: false,
|
|
345
|
-
revokedAt: receivedAt,
|
|
346
|
-
revokedBy: document.source.actor,
|
|
347
|
-
updatedAt: receivedAt,
|
|
348
|
-
updatedBy: document.source.actor
|
|
349
|
-
} });
|
|
350
|
-
}
|
|
351
|
-
for (const grant of document.grants) {
|
|
352
|
-
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
353
|
-
const existing = current.find((row) => row.id === id4);
|
|
354
|
-
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
355
|
-
id: id4,
|
|
356
|
-
sourceGrantId: grant.id,
|
|
357
|
-
sourceScope: "parent",
|
|
358
|
-
sourceId: document.source.appId,
|
|
359
|
-
targetId: document.target.chapterId,
|
|
360
|
-
email: grant.email,
|
|
361
|
-
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
362
|
-
decision: grant.decision,
|
|
363
|
-
...grant.note ? { note: grant.note } : {},
|
|
364
|
-
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
365
|
-
active: true,
|
|
366
|
-
sourceRevision: document.revision,
|
|
367
|
-
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
368
|
-
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
369
|
-
updatedAt: receivedAt,
|
|
370
|
-
updatedBy: document.source.actor
|
|
371
|
-
} });
|
|
372
|
-
const retract = [
|
|
373
|
-
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
374
|
-
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
375
|
-
];
|
|
376
|
-
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
377
|
-
}
|
|
378
|
-
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
379
|
-
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
380
|
-
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
381
|
-
id: revisionId,
|
|
382
|
-
sourceId: document.source.appId,
|
|
383
|
-
targetId: document.target.chapterId,
|
|
384
|
-
environment: document.target.environment,
|
|
385
|
-
revision: document.revision,
|
|
386
|
-
digest,
|
|
387
|
-
active: true,
|
|
388
|
-
receivedAt
|
|
389
|
-
} });
|
|
390
|
-
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
391
|
-
}
|
|
392
195
|
|
|
393
196
|
// src/member.ts
|
|
394
197
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
@@ -441,9 +244,9 @@ function resolveApplication(a) {
|
|
|
441
244
|
validateEmail: a?.validateEmail ?? true
|
|
442
245
|
};
|
|
443
246
|
}
|
|
444
|
-
var
|
|
247
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
445
248
|
function isValidEmail(value) {
|
|
446
|
-
return typeof value === "string" &&
|
|
249
|
+
return typeof value === "string" && EMAIL_RE.test(value);
|
|
447
250
|
}
|
|
448
251
|
function clampArray(value, max) {
|
|
449
252
|
if (!Array.isArray(value)) return value;
|
|
@@ -478,7 +281,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
478
281
|
const cap = app.maxLen[f] ?? app.defaultMaxLen;
|
|
479
282
|
if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
|
|
480
283
|
}
|
|
481
|
-
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
|
|
284
|
+
if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email.trim())) {
|
|
482
285
|
return { ok: false, error: "email must be a valid email address" };
|
|
483
286
|
}
|
|
484
287
|
const acked = hasDisclaimerAck(fields);
|
|
@@ -494,7 +297,7 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
494
297
|
if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
495
298
|
if (opts.groupId) row.groupId = opts.groupId;
|
|
496
299
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
497
|
-
row.emailNormalized =
|
|
300
|
+
row.emailNormalized = normalizeChapterEmail(fields.email);
|
|
498
301
|
}
|
|
499
302
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
500
303
|
row.admissionGrantId = opts.admissionGrant.id;
|
|
@@ -1556,6 +1359,82 @@ function formatChapterCopy(template, values) {
|
|
|
1556
1359
|
);
|
|
1557
1360
|
}
|
|
1558
1361
|
|
|
1362
|
+
// src/flow-fields.ts
|
|
1363
|
+
var CHAPTER_FLOW_FIELDS_SCHEMA_VERSION = 1;
|
|
1364
|
+
var APPLICATION_LABELS = {
|
|
1365
|
+
firstName: "First name",
|
|
1366
|
+
lastName: "Last name",
|
|
1367
|
+
email: "Email",
|
|
1368
|
+
referral: "Referral source",
|
|
1369
|
+
referralName: "Referral name",
|
|
1370
|
+
whoYouAre: "Who you are",
|
|
1371
|
+
linkedin: "LinkedIn",
|
|
1372
|
+
message: "Intro message",
|
|
1373
|
+
phone: "Phone",
|
|
1374
|
+
state: "State"
|
|
1375
|
+
};
|
|
1376
|
+
function humanize(id4) {
|
|
1377
|
+
const text3 = id4.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1378
|
+
return text3 ? `${text3[0].toUpperCase()}${text3.slice(1)}` : id4;
|
|
1379
|
+
}
|
|
1380
|
+
function applicationDefs(chapter) {
|
|
1381
|
+
try {
|
|
1382
|
+
return chapter.crm.type("person").fields;
|
|
1383
|
+
} catch {
|
|
1384
|
+
return {};
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
function applicationType(id4, def) {
|
|
1388
|
+
if (def && ["string", "email", "url", "enum"].includes(def.type)) return def.type;
|
|
1389
|
+
return id4 === "email" ? "email" : id4 === "linkedin" ? "url" : "string";
|
|
1390
|
+
}
|
|
1391
|
+
function applicationFlowFields(chapter, application = chapter.application) {
|
|
1392
|
+
const defs = applicationDefs(chapter);
|
|
1393
|
+
return [...application.required, ...application.optional].map((id4, order) => {
|
|
1394
|
+
const def = defs[id4];
|
|
1395
|
+
const conditions = application.conditions[id4];
|
|
1396
|
+
const type = applicationType(id4, def);
|
|
1397
|
+
return {
|
|
1398
|
+
id: id4,
|
|
1399
|
+
label: def?.label ?? APPLICATION_LABELS[id4] ?? humanize(id4),
|
|
1400
|
+
type,
|
|
1401
|
+
required: application.required.includes(id4),
|
|
1402
|
+
order,
|
|
1403
|
+
maxLength: application.maxLen[id4] ?? application.defaultMaxLen,
|
|
1404
|
+
...type === "enum" && def?.options ? { options: [...def.options] } : {},
|
|
1405
|
+
...conditions ? { conditions: { ...conditions } } : {},
|
|
1406
|
+
...type === "email" ? { emailIdentity: "literal" } : {}
|
|
1407
|
+
};
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
function resolveFormationFlowFields(crm, formation) {
|
|
1411
|
+
if (!formation.enabled) return [];
|
|
1412
|
+
const def = crm.type(formation.type);
|
|
1413
|
+
return [...formation.required, ...formation.optional].map((id4, order) => {
|
|
1414
|
+
const field = def.fields[id4];
|
|
1415
|
+
return {
|
|
1416
|
+
id: id4,
|
|
1417
|
+
label: field.label ?? humanize(id4),
|
|
1418
|
+
type: field.type,
|
|
1419
|
+
required: formation.required.includes(id4),
|
|
1420
|
+
order,
|
|
1421
|
+
maxLength: formation.maxLen[id4] ?? formation.defaultMaxLen,
|
|
1422
|
+
...field.options ? { options: [...field.options] } : {},
|
|
1423
|
+
...id4 === def.emailField || field.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1424
|
+
};
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
function formationFlowFields(chapter, formation = chapter.formation) {
|
|
1428
|
+
return resolveFormationFlowFields(chapter.crm, formation);
|
|
1429
|
+
}
|
|
1430
|
+
function chapterFlowFieldContract(chapter, flow, application = chapter.application) {
|
|
1431
|
+
return {
|
|
1432
|
+
schemaVersion: CHAPTER_FLOW_FIELDS_SCHEMA_VERSION,
|
|
1433
|
+
flow,
|
|
1434
|
+
fields: flow === "join" ? applicationFlowFields(chapter, application) : formationFlowFields(chapter)
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1559
1438
|
// src/formation.ts
|
|
1560
1439
|
var DEFAULT_REQUIRED2 = ["name", "email", "location", "thesis"];
|
|
1561
1440
|
var DEFAULT_OPTIONAL2 = ["proposedChapter", "experience", "collaborators"];
|
|
@@ -1618,17 +1497,7 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1618
1497
|
};
|
|
1619
1498
|
}
|
|
1620
1499
|
function formationFields(crm, formation) {
|
|
1621
|
-
|
|
1622
|
-
return [...formation.required, ...formation.optional].map((id4) => {
|
|
1623
|
-
const field = def.fields[id4];
|
|
1624
|
-
return {
|
|
1625
|
-
id: id4,
|
|
1626
|
-
label: field.label ?? id4,
|
|
1627
|
-
type: field.type,
|
|
1628
|
-
required: formation.required.includes(id4),
|
|
1629
|
-
...field.options ? { options: field.options } : {}
|
|
1630
|
-
};
|
|
1631
|
-
});
|
|
1500
|
+
return resolveFormationFlowFields(crm, formation);
|
|
1632
1501
|
}
|
|
1633
1502
|
|
|
1634
1503
|
// src/config-network.ts
|
|
@@ -2207,7 +2076,7 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
2207
2076
|
}
|
|
2208
2077
|
|
|
2209
2078
|
// src/member-content-validation-helpers.ts
|
|
2210
|
-
var
|
|
2079
|
+
var ID = /^[a-z][a-z0-9-]{0,79}$/;
|
|
2211
2080
|
var SLUG5 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2212
2081
|
var ROOT_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "revision", "generatedAt", "source", "target", "pages", "upsellOverrides"]);
|
|
2213
2082
|
var SOURCE_KEYS = /* @__PURE__ */ new Set(["type", "appId"]);
|
|
@@ -2247,7 +2116,7 @@ function offerValue(value, path, errors, tiers2, audience) {
|
|
|
2247
2116
|
}
|
|
2248
2117
|
unknownKeys(raw, OFFER_KEYS, path, errors);
|
|
2249
2118
|
const tierId = textValue(raw.tierId, `${path}.tierId`, errors, 80, true) ?? "";
|
|
2250
|
-
if (tierId && !
|
|
2119
|
+
if (tierId && !ID.test(tierId)) errors.push(`${path}.tierId must be a lowercase kebab-case id`);
|
|
2251
2120
|
if (tiers2.size && !tiers2.has(tierId)) errors.push(`${path}.tierId must reference a declared tier`);
|
|
2252
2121
|
if (audience?.kind === "tiers" && tierId && !audience.tierIds.includes(tierId)) {
|
|
2253
2122
|
errors.push(`${path}.tierId must grant access to this page`);
|
|
@@ -2280,7 +2149,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2280
2149
|
const type = sourceRaw.type;
|
|
2281
2150
|
const appId = textValue(sourceRaw.appId, "document.source.appId", errors, 63, true) ?? "";
|
|
2282
2151
|
if (type !== "parent" && type !== "chapter") errors.push("document.source.type must be parent or chapter");
|
|
2283
|
-
if (appId && !
|
|
2152
|
+
if (appId && !ID.test(appId)) errors.push("document.source.appId must be a lowercase app slug");
|
|
2284
2153
|
if (options.sourceType && type !== options.sourceType) errors.push(`document.source.type must be ${options.sourceType}`);
|
|
2285
2154
|
if (options.sourceAppId && appId !== options.sourceAppId) errors.push(`document.source.appId must be ${options.sourceAppId}`);
|
|
2286
2155
|
if (type === "parent" || type === "chapter") source = { type, appId };
|
|
@@ -2317,7 +2186,7 @@ function validateMemberContentDocument(input, options = {}) {
|
|
|
2317
2186
|
const id4 = textValue(raw.id, `${path}.id`, errors, 80, true) ?? "";
|
|
2318
2187
|
const slug = textValue(raw.slug, `${path}.slug`, errors, 120, true) ?? "";
|
|
2319
2188
|
const title = textValue(raw.title, `${path}.title`, errors, MAX_TEXT, true) ?? "";
|
|
2320
|
-
if (id4 && !
|
|
2189
|
+
if (id4 && !ID.test(id4)) errors.push(`${path}.id must be a lowercase kebab-case id`);
|
|
2321
2190
|
if (slug && !SLUG5.test(slug)) errors.push(`${path}.slug must be a lowercase URL slug`);
|
|
2322
2191
|
if (ids.has(id4) || knownParentIds.has(id4)) errors.push(`${path}.id duplicates page "${id4}"`);
|
|
2323
2192
|
if (slugs.has(slug) || (options.parentPages ?? []).some((page) => page.slug === slug)) errors.push(`${path}.slug duplicates "${slug}"`);
|
|
@@ -2497,25 +2366,25 @@ function projectMemberContent(content, viewer, tiers2) {
|
|
|
2497
2366
|
}
|
|
2498
2367
|
|
|
2499
2368
|
// src/member-content-canonical.ts
|
|
2500
|
-
var
|
|
2501
|
-
function
|
|
2369
|
+
var encoder = new TextEncoder();
|
|
2370
|
+
function canonicalValue(value) {
|
|
2502
2371
|
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2503
2372
|
return JSON.stringify(value);
|
|
2504
2373
|
}
|
|
2505
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2374
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue).join(",")}]`;
|
|
2506
2375
|
if (typeof value !== "object") throw new TypeError("member content contains an unsupported value");
|
|
2507
2376
|
const record3 = value;
|
|
2508
2377
|
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2509
2378
|
const item = record3[key];
|
|
2510
|
-
return item === void 0 ? [] : [`${JSON.stringify(key)}:${
|
|
2379
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue(item)}`];
|
|
2511
2380
|
});
|
|
2512
2381
|
return `{${entries.join(",")}}`;
|
|
2513
2382
|
}
|
|
2514
2383
|
function canonicalMemberContent(value) {
|
|
2515
|
-
return
|
|
2384
|
+
return canonicalValue(value);
|
|
2516
2385
|
}
|
|
2517
2386
|
async function memberContentDigest(document) {
|
|
2518
|
-
const digest = await crypto.subtle.digest("SHA-256",
|
|
2387
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(canonicalMemberContent(document)));
|
|
2519
2388
|
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2520
2389
|
}
|
|
2521
2390
|
async function createMemberContentEnvelope(document) {
|
|
@@ -2695,6 +2564,213 @@ function canceledPatch() {
|
|
|
2695
2564
|
return { canceled: true };
|
|
2696
2565
|
}
|
|
2697
2566
|
|
|
2567
|
+
// src/admission-grants-contract.ts
|
|
2568
|
+
var ADMISSION_GRANTS_SCHEMA_VERSION = 1;
|
|
2569
|
+
var ADMISSION_GRANTS_PATH = "/api/admission-grants";
|
|
2570
|
+
|
|
2571
|
+
// src/signup-control-canonical.ts
|
|
2572
|
+
var encoder2 = new TextEncoder();
|
|
2573
|
+
function canonicalValue2(value) {
|
|
2574
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2575
|
+
return JSON.stringify(value);
|
|
2576
|
+
}
|
|
2577
|
+
if (Array.isArray(value)) return `[${value.map(canonicalValue2).join(",")}]`;
|
|
2578
|
+
if (typeof value !== "object") throw new TypeError("signup control contains an unsupported value");
|
|
2579
|
+
const record3 = value;
|
|
2580
|
+
const entries = Object.keys(record3).sort().flatMap((key) => {
|
|
2581
|
+
const item = record3[key];
|
|
2582
|
+
return item === void 0 ? [] : [`${JSON.stringify(key)}:${canonicalValue2(item)}`];
|
|
2583
|
+
});
|
|
2584
|
+
return `{${entries.join(",")}}`;
|
|
2585
|
+
}
|
|
2586
|
+
function canonicalSignupControl(value) {
|
|
2587
|
+
return canonicalValue2(value);
|
|
2588
|
+
}
|
|
2589
|
+
async function signupControlDigest(document) {
|
|
2590
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(canonicalSignupControl(document)));
|
|
2591
|
+
return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
2592
|
+
}
|
|
2593
|
+
async function createSignupControlEnvelope(document) {
|
|
2594
|
+
return { document, digest: await signupControlDigest(document) };
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// src/admission-grants.ts
|
|
2598
|
+
var ID2 = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
2599
|
+
var MAX_GRANTS = 2e3;
|
|
2600
|
+
var MAX_NOTE = 2e3;
|
|
2601
|
+
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2602
|
+
var isAdmissionEmail = (value) => EMAIL_RE2.test(value);
|
|
2603
|
+
function normalizeAdmissionEmail(email) {
|
|
2604
|
+
return normalizeChapterEmail(email);
|
|
2605
|
+
}
|
|
2606
|
+
function validateAdmissionGrantDocument(input) {
|
|
2607
|
+
const errors = [];
|
|
2608
|
+
const root = input && typeof input === "object" ? input : {};
|
|
2609
|
+
if (root.schemaVersion !== ADMISSION_GRANTS_SCHEMA_VERSION) errors.push("document.schemaVersion must be 1");
|
|
2610
|
+
const revision = Number(root.revision);
|
|
2611
|
+
const generatedAt = Number(root.generatedAt);
|
|
2612
|
+
if (!Number.isSafeInteger(revision) || revision < 1) errors.push("document.revision must be a positive integer");
|
|
2613
|
+
if (!Number.isFinite(generatedAt) || generatedAt <= 0) errors.push("document.generatedAt must be epoch milliseconds");
|
|
2614
|
+
const source = root.source && typeof root.source === "object" ? root.source : {};
|
|
2615
|
+
const target = root.target && typeof root.target === "object" ? root.target : {};
|
|
2616
|
+
const sourceId = typeof source.appId === "string" ? source.appId : "";
|
|
2617
|
+
const sourceActor = typeof source.actor === "string" ? source.actor.trim() : "";
|
|
2618
|
+
const chapterId = typeof target.chapterId === "string" ? target.chapterId : "";
|
|
2619
|
+
const environment = typeof target.environment === "string" ? target.environment : "";
|
|
2620
|
+
if (!ID2.test(sourceId)) errors.push("document.source.appId must be an identifier");
|
|
2621
|
+
if (!sourceActor || sourceActor.length > 256) errors.push("document.source.actor must be 1-256 characters");
|
|
2622
|
+
if (!ID2.test(chapterId)) errors.push("document.target.chapterId must be an identifier");
|
|
2623
|
+
if (!ID2.test(environment)) errors.push("document.target.environment must be an identifier");
|
|
2624
|
+
if (!Array.isArray(root.grants)) errors.push("document.grants must be an array");
|
|
2625
|
+
const rawGrants = Array.isArray(root.grants) ? root.grants : [];
|
|
2626
|
+
if (rawGrants.length > MAX_GRANTS) errors.push(`document.grants supports at most ${MAX_GRANTS} entries`);
|
|
2627
|
+
const grants = [];
|
|
2628
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2629
|
+
const seenEmails = /* @__PURE__ */ new Set();
|
|
2630
|
+
for (const [index, raw] of rawGrants.slice(0, MAX_GRANTS).entries()) {
|
|
2631
|
+
const grant = raw && typeof raw === "object" ? raw : {};
|
|
2632
|
+
const id4 = typeof grant.id === "string" ? grant.id : "";
|
|
2633
|
+
const email = typeof grant.email === "string" ? grant.email.trim() : "";
|
|
2634
|
+
const normalized = normalizeAdmissionEmail(email);
|
|
2635
|
+
const decision = grant.decision;
|
|
2636
|
+
const note = typeof grant.note === "string" ? grant.note.trim() : void 0;
|
|
2637
|
+
const expiresAt = grant.expiresAt === void 0 ? void 0 : Number(grant.expiresAt);
|
|
2638
|
+
if (!ID2.test(id4)) errors.push(`document.grants[${index}].id must be an identifier`);
|
|
2639
|
+
if (seenIds.has(id4)) errors.push(`document.grants repeats id "${id4}"`);
|
|
2640
|
+
if (!isAdmissionEmail(normalized)) errors.push(`document.grants[${index}].email must be valid`);
|
|
2641
|
+
if (seenEmails.has(normalized)) errors.push(`document.grants repeats email "${normalized}"`);
|
|
2642
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
2643
|
+
errors.push(`document.grants[${index}].decision is invalid`);
|
|
2644
|
+
}
|
|
2645
|
+
if (note && note.length > MAX_NOTE) errors.push(`document.grants[${index}].note is too long`);
|
|
2646
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= 0)) {
|
|
2647
|
+
errors.push(`document.grants[${index}].expiresAt must be epoch milliseconds`);
|
|
2648
|
+
}
|
|
2649
|
+
seenIds.add(id4);
|
|
2650
|
+
seenEmails.add(normalized);
|
|
2651
|
+
grants.push({
|
|
2652
|
+
id: id4,
|
|
2653
|
+
email,
|
|
2654
|
+
decision,
|
|
2655
|
+
...note ? { note } : {},
|
|
2656
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
if (errors.length) return { ok: false, errors };
|
|
2660
|
+
return {
|
|
2661
|
+
ok: true,
|
|
2662
|
+
value: {
|
|
2663
|
+
schemaVersion: ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
2664
|
+
revision,
|
|
2665
|
+
generatedAt,
|
|
2666
|
+
source: { appId: sourceId, actor: sourceActor },
|
|
2667
|
+
target: { chapterId, environment },
|
|
2668
|
+
grants
|
|
2669
|
+
}
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
async function createAdmissionGrantEnvelope(document) {
|
|
2673
|
+
const checked = validateAdmissionGrantDocument(document);
|
|
2674
|
+
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
2675
|
+
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
2676
|
+
}
|
|
2677
|
+
var rows = (value) => Array.isArray(value) ? value : [];
|
|
2678
|
+
function asGrant(row) {
|
|
2679
|
+
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
2680
|
+
if (row.sourceScope !== "local" && row.sourceScope !== "parent") return null;
|
|
2681
|
+
if (row.decision !== "waive_interview" && row.decision !== "require_interview") return null;
|
|
2682
|
+
return row;
|
|
2683
|
+
}
|
|
2684
|
+
async function findAdmissionGrant(db, targetId, email, now) {
|
|
2685
|
+
const emailNormalized = normalizeAdmissionEmail(email);
|
|
2686
|
+
if (!isAdmissionEmail(emailNormalized)) return null;
|
|
2687
|
+
const result = await db.query({ admissionGrants: { $: { where: { emailNormalized }, limit: 100 } } });
|
|
2688
|
+
const active = rows(result.admissionGrants).map(asGrant).filter((grant) => Boolean(
|
|
2689
|
+
grant && grant.targetId === targetId && grant.active === true && !grant.revokedAt && (!grant.expiresAt || grant.expiresAt > now) && !grant.consumedApplicationId
|
|
2690
|
+
)).sort((a, b) => {
|
|
2691
|
+
const scope = Number(b.sourceScope === "local") - Number(a.sourceScope === "local");
|
|
2692
|
+
return scope || b.createdAt - a.createdAt;
|
|
2693
|
+
});
|
|
2694
|
+
return active[0] ?? null;
|
|
2695
|
+
}
|
|
2696
|
+
async function hasAdmissionRevision(db, sourceId, targetId, environment) {
|
|
2697
|
+
const where = { sourceId, targetId, environment };
|
|
2698
|
+
if (db.aggregate) {
|
|
2699
|
+
return Number((await db.aggregate("admissionGrantRevisions", { count: true }, { where })).count ?? 0) > 0;
|
|
2700
|
+
}
|
|
2701
|
+
const found = await db.query({ admissionGrantRevisions: {} });
|
|
2702
|
+
return rows(found.admissionGrantRevisions).some((row) => row.sourceId === sourceId && row.targetId === targetId && row.environment === environment);
|
|
2703
|
+
}
|
|
2704
|
+
async function loadLatestAdmissionRevision(db, sourceId, targetId, environment) {
|
|
2705
|
+
if (!await hasAdmissionRevision(db, sourceId, targetId, environment)) return null;
|
|
2706
|
+
const result = await db.query({ admissionGrantRevisions: { $: {
|
|
2707
|
+
where: { sourceId, targetId, environment },
|
|
2708
|
+
order: { revision: "desc" },
|
|
2709
|
+
limit: 1
|
|
2710
|
+
} } });
|
|
2711
|
+
const row = rows(result.admissionGrantRevisions).sort((a, b) => Number(b.revision ?? 0) - Number(a.revision ?? 0))[0];
|
|
2712
|
+
return row ? row : null;
|
|
2713
|
+
}
|
|
2714
|
+
async function materializeAdmissionGrants(db, envelope, previous, receivedAt) {
|
|
2715
|
+
const { document, digest } = envelope;
|
|
2716
|
+
const current = rows((await db.query({ admissionGrants: { $: {
|
|
2717
|
+
where: { sourceScope: "parent", sourceId: document.source.appId, targetId: document.target.chapterId },
|
|
2718
|
+
limit: MAX_GRANTS
|
|
2719
|
+
} } })).admissionGrants);
|
|
2720
|
+
const incomingIds = new Set(document.grants.map((grant) => `admission:parent:${document.source.appId}:${grant.id}`));
|
|
2721
|
+
const ops = [];
|
|
2722
|
+
for (const row of current) {
|
|
2723
|
+
const id4 = String(row.id ?? "");
|
|
2724
|
+
if (id4 && !incomingIds.has(id4)) ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
2725
|
+
active: false,
|
|
2726
|
+
revokedAt: receivedAt,
|
|
2727
|
+
revokedBy: document.source.actor,
|
|
2728
|
+
updatedAt: receivedAt,
|
|
2729
|
+
updatedBy: document.source.actor
|
|
2730
|
+
} });
|
|
2731
|
+
}
|
|
2732
|
+
for (const grant of document.grants) {
|
|
2733
|
+
const id4 = `admission:parent:${document.source.appId}:${grant.id}`;
|
|
2734
|
+
const existing = current.find((row) => row.id === id4);
|
|
2735
|
+
ops.push({ t: "update", ns: "admissionGrants", id: id4, attrs: {
|
|
2736
|
+
id: id4,
|
|
2737
|
+
sourceGrantId: grant.id,
|
|
2738
|
+
sourceScope: "parent",
|
|
2739
|
+
sourceId: document.source.appId,
|
|
2740
|
+
targetId: document.target.chapterId,
|
|
2741
|
+
email: grant.email,
|
|
2742
|
+
emailNormalized: normalizeAdmissionEmail(grant.email),
|
|
2743
|
+
decision: grant.decision,
|
|
2744
|
+
...grant.note ? { note: grant.note } : {},
|
|
2745
|
+
...grant.expiresAt ? { expiresAt: grant.expiresAt } : {},
|
|
2746
|
+
active: true,
|
|
2747
|
+
sourceRevision: document.revision,
|
|
2748
|
+
createdAt: Number(existing?.createdAt ?? receivedAt),
|
|
2749
|
+
createdBy: String(existing?.createdBy ?? document.source.actor),
|
|
2750
|
+
updatedAt: receivedAt,
|
|
2751
|
+
updatedBy: document.source.actor
|
|
2752
|
+
} });
|
|
2753
|
+
const retract = [
|
|
2754
|
+
...!grant.note && existing?.note !== void 0 ? ["note"] : [],
|
|
2755
|
+
...grant.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
2756
|
+
];
|
|
2757
|
+
if (retract.length) ops.push({ t: "retract", ns: "admissionGrants", id: id4, attrs: retract });
|
|
2758
|
+
}
|
|
2759
|
+
if (previous?.active) ops.push({ t: "update", ns: "admissionGrantRevisions", id: previous.id, attrs: { active: false } });
|
|
2760
|
+
const revisionId = `admission-revision:${document.source.appId}:${document.target.environment}:${document.revision}`;
|
|
2761
|
+
ops.push({ t: "update", ns: "admissionGrantRevisions", id: revisionId, attrs: {
|
|
2762
|
+
id: revisionId,
|
|
2763
|
+
sourceId: document.source.appId,
|
|
2764
|
+
targetId: document.target.chapterId,
|
|
2765
|
+
environment: document.target.environment,
|
|
2766
|
+
revision: document.revision,
|
|
2767
|
+
digest,
|
|
2768
|
+
active: true,
|
|
2769
|
+
receivedAt
|
|
2770
|
+
} });
|
|
2771
|
+
await db.transact(ops, { mutationId: `admission-grants:${document.source.appId}:${document.target.environment}:${document.revision}:${digest}` });
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2698
2774
|
// src/join-continuation.ts
|
|
2699
2775
|
function resolveJoinContinuation(input) {
|
|
2700
2776
|
if (input.status === "approved") return "accepted";
|