@odla-ai/chapter 0.42.1 → 0.42.3
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 +13 -1
- package/dist/{chunk-JXFYOUDL.js → chunk-4HS2JPE2.js} +11 -1
- package/dist/chunk-4HS2JPE2.js.map +1 -0
- package/dist/{chunk-MMDWXTJU.js → chunk-7MEVZEL2.js} +49 -3
- package/dist/chunk-7MEVZEL2.js.map +1 -0
- package/dist/{chunk-S7CND3R5.js → chunk-VWDEHUNI.js} +2 -2
- package/dist/{copy-context-BNUzbL5g.d.ts → copy-context-Bh1EZvCg.d.ts} +10 -2
- package/dist/index.cjs +98 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.js +98 -10
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +14 -3
- package/dist/ui/admin/index.js +4 -2
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +5 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +551 -156
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +56 -3
- package/dist/worker/index.d.ts +56 -3
- package/dist/worker/index.js +527 -132
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-JXFYOUDL.js.map +0 -1
- package/dist/chunk-MMDWXTJU.js.map +0 -1
- /package/dist/{chunk-S7CND3R5.js.map → chunk-VWDEHUNI.js.map} +0 -0
package/dist/worker/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/worker.ts
|
|
21
21
|
var worker_exports = {};
|
|
22
22
|
__export(worker_exports, {
|
|
23
|
+
AdmissionGrantDeliveryError: () => AdmissionGrantDeliveryError,
|
|
23
24
|
chapterWorker: () => chapterWorker,
|
|
24
25
|
createWorkerContext: () => createWorkerContext,
|
|
25
26
|
deliverAdmissionGrants: () => deliverAdmissionGrants,
|
|
@@ -27,6 +28,7 @@ __export(worker_exports, {
|
|
|
27
28
|
deliverMemberContent: () => deliverMemberContent,
|
|
28
29
|
deliverSignupControl: () => deliverSignupControl,
|
|
29
30
|
renderActiveSignupControlHtml: () => renderActiveSignupControlHtml,
|
|
31
|
+
retryLeaderAdmissionDeliveries: () => retryLeaderAdmissionDeliveries,
|
|
30
32
|
withActiveSignupControlTheme: () => withActiveSignupControlTheme
|
|
31
33
|
});
|
|
32
34
|
module.exports = __toCommonJS(worker_exports);
|
|
@@ -640,6 +642,11 @@ function validateAdmissionGrantDocument(input) {
|
|
|
640
642
|
}
|
|
641
643
|
};
|
|
642
644
|
}
|
|
645
|
+
async function createAdmissionGrantEnvelope(document) {
|
|
646
|
+
const checked = validateAdmissionGrantDocument(document);
|
|
647
|
+
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
648
|
+
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
649
|
+
}
|
|
643
650
|
var rows = (value) => Array.isArray(value) ? value : [];
|
|
644
651
|
function asGrant(row) {
|
|
645
652
|
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
@@ -3138,12 +3145,17 @@ var import_db5 = require("@odla-ai/db");
|
|
|
3138
3145
|
var reject3 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
|
|
3139
3146
|
var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
3140
3147
|
if (req.method !== "POST" || url.pathname !== ADMISSION_GRANTS_PATH) return null;
|
|
3141
|
-
const
|
|
3142
|
-
|
|
3148
|
+
const claimedSender = req.headers.get("x-odla-federation-sender") ?? "";
|
|
3149
|
+
const networkReader = ctx.chapter.network.readers.find((reader) => reader.id === claimedSender && reader.admissionGrants);
|
|
3150
|
+
const signupAuthority = ctx.chapter.signupControl?.sourceId === claimedSender ? ctx.chapter.signupControl : void 0;
|
|
3151
|
+
if (!networkReader && !signupAuthority) return json({ error: "not found" }, 404);
|
|
3143
3152
|
const db = ctx.makeDb(env);
|
|
3144
|
-
const secret = await getVaultSecret(
|
|
3153
|
+
const secret = await getVaultSecret(
|
|
3154
|
+
db,
|
|
3155
|
+
networkReader ? "network_share_secret" : signupAuthority.secretName
|
|
3156
|
+
);
|
|
3145
3157
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
3146
|
-
const verified = await (0, import_db5.verifyFederatedRequest)(req, { secret, senders: [
|
|
3158
|
+
const verified = await (0, import_db5.verifyFederatedRequest)(req, { secret, senders: [claimedSender] });
|
|
3147
3159
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
3148
3160
|
let input;
|
|
3149
3161
|
try {
|
|
@@ -3155,11 +3167,11 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3155
3167
|
const checked = validateAdmissionGrantDocument(input.document);
|
|
3156
3168
|
if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
|
|
3157
3169
|
const document = checked.value;
|
|
3158
|
-
if (document.source.appId !==
|
|
3170
|
+
if (document.source.appId !== claimedSender) return reject3("wrong_source", 403);
|
|
3159
3171
|
if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
|
|
3160
3172
|
if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
|
|
3161
3173
|
if (!digest || await signupControlDigest(document) !== digest) return reject3("invalid_digest", 400);
|
|
3162
|
-
const latest = await loadLatestAdmissionRevision(db,
|
|
3174
|
+
const latest = await loadLatestAdmissionRevision(db, claimedSender, ctx.chapter.id, env.ODLA_ENV);
|
|
3163
3175
|
if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
|
|
3164
3176
|
if (latest && document.revision === latest.revision) {
|
|
3165
3177
|
if (latest.digest !== digest) return reject3("conflicting_revision", 409);
|
|
@@ -3284,6 +3296,384 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3284
3296
|
return null;
|
|
3285
3297
|
};
|
|
3286
3298
|
|
|
3299
|
+
// src/worker-routes-network-admission.ts
|
|
3300
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
3301
|
+
|
|
3302
|
+
// src/admission-grants-delivery.ts
|
|
3303
|
+
var import_db6 = require("@odla-ai/db");
|
|
3304
|
+
var AdmissionGrantDeliveryError = class extends Error {
|
|
3305
|
+
constructor(status, reason) {
|
|
3306
|
+
super(`admission grants rejected (${status}): ${reason}`);
|
|
3307
|
+
this.status = status;
|
|
3308
|
+
this.reason = reason;
|
|
3309
|
+
this.name = "AdmissionGrantDeliveryError";
|
|
3310
|
+
}
|
|
3311
|
+
status;
|
|
3312
|
+
reason;
|
|
3313
|
+
};
|
|
3314
|
+
async function deliverAdmissionGrants(options) {
|
|
3315
|
+
const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
|
|
3316
|
+
const body = JSON.stringify(options.envelope);
|
|
3317
|
+
const signed = await (0, import_db6.signFederatedRequest)({
|
|
3318
|
+
secret: options.secret,
|
|
3319
|
+
sender: options.sender,
|
|
3320
|
+
method: "POST",
|
|
3321
|
+
url: destination,
|
|
3322
|
+
body
|
|
3323
|
+
});
|
|
3324
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
3325
|
+
method: "POST",
|
|
3326
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
3327
|
+
body
|
|
3328
|
+
});
|
|
3329
|
+
const result = await response.json();
|
|
3330
|
+
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
3331
|
+
const failure = result;
|
|
3332
|
+
throw new AdmissionGrantDeliveryError(
|
|
3333
|
+
response.status,
|
|
3334
|
+
failure.reason ?? failure.error ?? "unknown"
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
return result;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// src/worker-network-fetch.ts
|
|
3341
|
+
var NetworkTargetTransportError = class extends Error {
|
|
3342
|
+
code = "binding_unavailable";
|
|
3343
|
+
binding;
|
|
3344
|
+
constructor(binding) {
|
|
3345
|
+
super(`service binding "${binding}" is unavailable`);
|
|
3346
|
+
this.name = "NetworkTargetTransportError";
|
|
3347
|
+
this.binding = binding;
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
function resolveNetworkTargetTransport(env, target) {
|
|
3351
|
+
if (!target.binding) {
|
|
3352
|
+
return {
|
|
3353
|
+
kind: "url",
|
|
3354
|
+
fetch: (destination, init) => globalThis.fetch(destination, init)
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
const candidate = env[target.binding];
|
|
3358
|
+
if (typeof candidate !== "object" || candidate === null || !("fetch" in candidate) || typeof candidate.fetch !== "function") {
|
|
3359
|
+
throw new NetworkTargetTransportError(target.binding);
|
|
3360
|
+
}
|
|
3361
|
+
const binding = candidate;
|
|
3362
|
+
return {
|
|
3363
|
+
kind: "binding",
|
|
3364
|
+
binding: target.binding,
|
|
3365
|
+
// Keep the receiver attached: Cloudflare service bindings require it.
|
|
3366
|
+
fetch: (destination, init) => binding.fetch(new Request(destination, init))
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
function fetchNetworkTarget(env, target, destination, init) {
|
|
3370
|
+
return resolveNetworkTargetTransport(env, target).fetch(destination, init);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/worker-network-admission-state.ts
|
|
3374
|
+
var import_crm5 = require("@odla-ai/crm");
|
|
3375
|
+
var MAX_ASSIGNMENTS = 2e3;
|
|
3376
|
+
var admissionRows = (value) => Array.isArray(value) ? value : [];
|
|
3377
|
+
function assignmentId(targetId, personId) {
|
|
3378
|
+
return `leader-admission:${targetId}:${personId}`;
|
|
3379
|
+
}
|
|
3380
|
+
function deliveryId(targetId, environment) {
|
|
3381
|
+
return `leader-admission-delivery:${targetId}:${environment}`;
|
|
3382
|
+
}
|
|
3383
|
+
async function loadLeaderAdmissionAssignments(db, targetId) {
|
|
3384
|
+
const result = await db.query({ leaderAdmissionAssignments: { $: {
|
|
3385
|
+
where: { targetId },
|
|
3386
|
+
order: { updatedAt: "desc" },
|
|
3387
|
+
limit: MAX_ASSIGNMENTS
|
|
3388
|
+
} } });
|
|
3389
|
+
return admissionRows(result.leaderAdmissionAssignments);
|
|
3390
|
+
}
|
|
3391
|
+
async function loadLeaderAdmissionDelivery(db, targetId, environment) {
|
|
3392
|
+
const id = deliveryId(targetId, environment);
|
|
3393
|
+
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id }, limit: 1 } } });
|
|
3394
|
+
return admissionRows(result.leaderAdmissionDeliveries)[0] ?? null;
|
|
3395
|
+
}
|
|
3396
|
+
function personAdmissionEmail(ctx, record4) {
|
|
3397
|
+
if (record4.type !== "person") return null;
|
|
3398
|
+
const field = ctx.chapter.crm.config.types.person?.emailField;
|
|
3399
|
+
if (!field) return null;
|
|
3400
|
+
const raw = record4.fields[field] ?? record4.primaryEmail;
|
|
3401
|
+
if (typeof raw !== "string") return null;
|
|
3402
|
+
const normalized = normalizeAdmissionEmail(raw);
|
|
3403
|
+
return isValidEmail(normalized) ? normalized : null;
|
|
3404
|
+
}
|
|
3405
|
+
async function grantInput(db, ctx, assignment) {
|
|
3406
|
+
const person = await (0, import_crm5.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
|
|
3407
|
+
const email = person ? personAdmissionEmail(ctx, person) : null;
|
|
3408
|
+
if (!email) throw new Error(`person ${assignment.personId} has no valid authoritative email`);
|
|
3409
|
+
return {
|
|
3410
|
+
id: assignment.grantId,
|
|
3411
|
+
email,
|
|
3412
|
+
decision: assignment.decision,
|
|
3413
|
+
...assignment.note ? { note: assignment.note } : {},
|
|
3414
|
+
...assignment.expiresAt !== void 0 ? { expiresAt: assignment.expiresAt } : {}
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
async function persistLeaderAdmissionDesiredSet(opts) {
|
|
3418
|
+
const now = Date.now();
|
|
3419
|
+
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
3420
|
+
const id = assignmentId(opts.target.id, opts.personId);
|
|
3421
|
+
const existing = current.find((row) => row.id === id);
|
|
3422
|
+
const digest = await signupControlDigest({
|
|
3423
|
+
targetId: opts.target.id,
|
|
3424
|
+
personId: opts.personId,
|
|
3425
|
+
decision: opts.decision,
|
|
3426
|
+
note: opts.note ?? null,
|
|
3427
|
+
expiresAt: opts.expiresAt ?? null,
|
|
3428
|
+
active: opts.active
|
|
3429
|
+
});
|
|
3430
|
+
const mutationReceiptId = `leader-admission-mutation:${opts.target.id}:${opts.mutationId}`;
|
|
3431
|
+
const priorMutation2 = await opts.db.query({ leaderAdmissionMutations: { $: {
|
|
3432
|
+
where: { id: mutationReceiptId },
|
|
3433
|
+
limit: 1
|
|
3434
|
+
} } });
|
|
3435
|
+
const receipt = admissionRows(priorMutation2.leaderAdmissionMutations)[0];
|
|
3436
|
+
if (receipt) {
|
|
3437
|
+
if (receipt.requestDigest !== digest || receipt.personId !== opts.personId) {
|
|
3438
|
+
throw new Error("mutation_conflict");
|
|
3439
|
+
}
|
|
3440
|
+
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3441
|
+
if (!saved) throw new Error("delivery_missing");
|
|
3442
|
+
return { delivery: saved, duplicate: true };
|
|
3443
|
+
}
|
|
3444
|
+
if (existing?.lastMutationId === opts.mutationId) {
|
|
3445
|
+
if (existing.lastRequestDigest !== digest) throw new Error("mutation_conflict");
|
|
3446
|
+
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3447
|
+
if (!saved) throw new Error("delivery_missing");
|
|
3448
|
+
return { delivery: saved, duplicate: true };
|
|
3449
|
+
}
|
|
3450
|
+
const assignment = {
|
|
3451
|
+
id,
|
|
3452
|
+
targetId: opts.target.id,
|
|
3453
|
+
personId: opts.personId,
|
|
3454
|
+
grantId: existing?.grantId ?? crypto.randomUUID().replaceAll("-", ""),
|
|
3455
|
+
decision: opts.decision,
|
|
3456
|
+
...opts.note ? { note: opts.note } : {},
|
|
3457
|
+
...opts.expiresAt !== void 0 ? { expiresAt: opts.expiresAt } : {},
|
|
3458
|
+
active: opts.active,
|
|
3459
|
+
lastMutationId: opts.mutationId,
|
|
3460
|
+
lastRequestDigest: digest,
|
|
3461
|
+
createdAt: existing?.createdAt ?? now,
|
|
3462
|
+
createdBy: existing?.createdBy ?? opts.actor,
|
|
3463
|
+
updatedAt: now,
|
|
3464
|
+
updatedBy: opts.actor
|
|
3465
|
+
};
|
|
3466
|
+
const desired = current.filter((row) => row.id !== id && row.active).concat(assignment.active ? [assignment] : []);
|
|
3467
|
+
const grants = await Promise.all(desired.map((row) => grantInput(opts.db, opts.ctx, row)));
|
|
3468
|
+
const previous = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3469
|
+
const revision = (previous?.revision ?? 0) + 1;
|
|
3470
|
+
const envelope = await createAdmissionGrantEnvelope({
|
|
3471
|
+
schemaVersion: 1,
|
|
3472
|
+
revision,
|
|
3473
|
+
generatedAt: now,
|
|
3474
|
+
source: { appId: opts.ctx.chapter.id, actor: opts.actor },
|
|
3475
|
+
target: { chapterId: opts.target.id, environment: opts.env.ODLA_ENV },
|
|
3476
|
+
grants
|
|
3477
|
+
});
|
|
3478
|
+
const delivery = {
|
|
3479
|
+
id: deliveryId(opts.target.id, opts.env.ODLA_ENV),
|
|
3480
|
+
targetId: opts.target.id,
|
|
3481
|
+
environment: opts.env.ODLA_ENV,
|
|
3482
|
+
revision,
|
|
3483
|
+
digest: envelope.digest,
|
|
3484
|
+
envelope,
|
|
3485
|
+
status: "saved",
|
|
3486
|
+
attempts: 0,
|
|
3487
|
+
updatedAt: now
|
|
3488
|
+
};
|
|
3489
|
+
const ops = [
|
|
3490
|
+
{ t: "update", ns: "leaderAdmissionAssignments", id, attrs: assignment },
|
|
3491
|
+
{ t: "update", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: delivery },
|
|
3492
|
+
{ t: "update", ns: "leaderAdmissionMutations", id: mutationReceiptId, attrs: {
|
|
3493
|
+
id: mutationReceiptId,
|
|
3494
|
+
targetId: opts.target.id,
|
|
3495
|
+
personId: opts.personId,
|
|
3496
|
+
requestDigest: digest,
|
|
3497
|
+
revision,
|
|
3498
|
+
createdAt: now
|
|
3499
|
+
} }
|
|
3500
|
+
];
|
|
3501
|
+
const retract = [
|
|
3502
|
+
...!opts.note && existing?.note !== void 0 ? ["note"] : [],
|
|
3503
|
+
...opts.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
3504
|
+
];
|
|
3505
|
+
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id, attrs: retract });
|
|
3506
|
+
const tx = await opts.db.transact(ops, {
|
|
3507
|
+
mutationId: `leader-admission:${opts.target.id}:${opts.mutationId}`,
|
|
3508
|
+
guards: previous ? [{ ns: "leaderAdmissionDeliveries", id: previous.id, exists: true, equals: { revision: previous.revision } }] : [{ ns: "leaderAdmissionDeliveries", id: delivery.id, exists: false }]
|
|
3509
|
+
});
|
|
3510
|
+
return { delivery, duplicate: tx.duplicate };
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
// src/worker-routes-network-admission.ts
|
|
3514
|
+
var PATH = "/api/admin/network/admissions";
|
|
3515
|
+
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
3516
|
+
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
3517
|
+
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
3518
|
+
var MAX_NOTE2 = 2e3;
|
|
3519
|
+
var reject4 = (status, code, error) => json({ ok: false, code, error }, status);
|
|
3520
|
+
async function gate2(req, env, ctx) {
|
|
3521
|
+
const db = ctx.makeDb(env);
|
|
3522
|
+
const user = await ctx.verifyUser(req, env);
|
|
3523
|
+
if (!user) return { response: reject4(401, "unauthorized", "unauthorized") };
|
|
3524
|
+
if (!await ctx.isAdmin(db, user)) return { response: reject4(403, "forbidden", "forbidden") };
|
|
3525
|
+
return { db, user };
|
|
3526
|
+
}
|
|
3527
|
+
function targetFor(ctx, id) {
|
|
3528
|
+
return ctx.chapter.network.targets.find((target) => target.id === id && target.admissionGrants);
|
|
3529
|
+
}
|
|
3530
|
+
async function deliverSaved(db, env, ctx, target, delivery) {
|
|
3531
|
+
if (delivery.status === "acknowledged") return delivery;
|
|
3532
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
3533
|
+
const attemptedAt = Date.now();
|
|
3534
|
+
const attempts = delivery.attempts + 1;
|
|
3535
|
+
let patch;
|
|
3536
|
+
if (!secret) {
|
|
3537
|
+
patch = {
|
|
3538
|
+
status: delivery.attempts > 0 ? "retrying" : "pending",
|
|
3539
|
+
attempts,
|
|
3540
|
+
lastAttemptAt: attemptedAt,
|
|
3541
|
+
lastError: "edge secret is missing",
|
|
3542
|
+
updatedAt: attemptedAt
|
|
3543
|
+
};
|
|
3544
|
+
} else {
|
|
3545
|
+
try {
|
|
3546
|
+
const receipt = await deliverAdmissionGrants({
|
|
3547
|
+
secret,
|
|
3548
|
+
sender: ctx.chapter.id,
|
|
3549
|
+
targetUrl: target.url,
|
|
3550
|
+
envelope: delivery.envelope,
|
|
3551
|
+
fetch: ((input, init) => fetchNetworkTarget(env, target, new URL(String(input)), init ?? {}))
|
|
3552
|
+
});
|
|
3553
|
+
patch = {
|
|
3554
|
+
status: "acknowledged",
|
|
3555
|
+
attempts,
|
|
3556
|
+
lastAttemptAt: attemptedAt,
|
|
3557
|
+
acknowledgedAt: receipt.acknowledgedAt,
|
|
3558
|
+
updatedAt: attemptedAt
|
|
3559
|
+
};
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
const permanent = error instanceof AdmissionGrantDeliveryError && error.status >= 400 && error.status < 500;
|
|
3562
|
+
patch = {
|
|
3563
|
+
status: permanent ? "rejected" : delivery.attempts > 0 ? "retrying" : "pending",
|
|
3564
|
+
attempts,
|
|
3565
|
+
lastAttemptAt: attemptedAt,
|
|
3566
|
+
lastError: error instanceof Error ? error.message.slice(0, 2e3) : "delivery failed",
|
|
3567
|
+
updatedAt: attemptedAt
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
const ops = [{
|
|
3572
|
+
t: "update",
|
|
3573
|
+
ns: "leaderAdmissionDeliveries",
|
|
3574
|
+
id: delivery.id,
|
|
3575
|
+
attrs: patch
|
|
3576
|
+
}];
|
|
3577
|
+
if (patch.status === "acknowledged" && delivery.lastError) {
|
|
3578
|
+
ops.push({ t: "retract", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: ["lastError"] });
|
|
3579
|
+
}
|
|
3580
|
+
await db.transact(ops, {
|
|
3581
|
+
mutationId: `leader-admission-attempt:${delivery.targetId}:${delivery.revision}:${attempts}:${patch.status}`
|
|
3582
|
+
});
|
|
3583
|
+
return { ...delivery, ...patch };
|
|
3584
|
+
}
|
|
3585
|
+
async function parseBody(req) {
|
|
3586
|
+
try {
|
|
3587
|
+
return await req.json();
|
|
3588
|
+
} catch {
|
|
3589
|
+
return null;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
3593
|
+
if (![PATH, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
3594
|
+
const auth = await gate2(req, env, ctx);
|
|
3595
|
+
if ("response" in auth) return auth.response;
|
|
3596
|
+
const db = auth.db;
|
|
3597
|
+
if (url.pathname === PATH && req.method === "GET") {
|
|
3598
|
+
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
3599
|
+
const target2 = targetFor(ctx, targetId2);
|
|
3600
|
+
if (!target2) return reject4(404, "unknown_target", "admission target is not configured");
|
|
3601
|
+
return json({
|
|
3602
|
+
ok: true,
|
|
3603
|
+
target: { id: target2.id, name: target2.name },
|
|
3604
|
+
assignments: await loadLeaderAdmissionAssignments(db, target2.id),
|
|
3605
|
+
delivery: await loadLeaderAdmissionDelivery(db, target2.id, env.ODLA_ENV)
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
if (req.method !== "POST") return null;
|
|
3609
|
+
const body = await parseBody(req);
|
|
3610
|
+
if (!body) return reject4(422, "invalid_request", "request body must be JSON");
|
|
3611
|
+
const targetId = typeof body.targetId === "string" ? body.targetId : "";
|
|
3612
|
+
const target = targetFor(ctx, targetId);
|
|
3613
|
+
if (!target) return reject4(404, "unknown_target", "admission target is not configured");
|
|
3614
|
+
if (url.pathname === RETRY_PATH) {
|
|
3615
|
+
const delivery = await loadLeaderAdmissionDelivery(db, target.id, env.ODLA_ENV);
|
|
3616
|
+
if (!delivery) return reject4(404, "not_found", "no saved admission delivery exists");
|
|
3617
|
+
return json({ ok: true, delivery: await deliverSaved(db, env, ctx, target, delivery) });
|
|
3618
|
+
}
|
|
3619
|
+
const personId = typeof body.personId === "string" ? body.personId : "";
|
|
3620
|
+
const mutationId = typeof body.mutationId === "string" ? body.mutationId : "";
|
|
3621
|
+
if (!personId || !MUTATION_ID.test(mutationId)) {
|
|
3622
|
+
return reject4(422, "invalid_request", "personId and a stable mutationId are required");
|
|
3623
|
+
}
|
|
3624
|
+
const person = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: auth.db }, personId);
|
|
3625
|
+
if (!person) return reject4(404, "person_not_found", "person not found");
|
|
3626
|
+
if (!personAdmissionEmail(ctx, person)) {
|
|
3627
|
+
return reject4(422, "invalid_email", "person has no valid authoritative email");
|
|
3628
|
+
}
|
|
3629
|
+
const decision = body.decision ?? "waive_interview";
|
|
3630
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
3631
|
+
return reject4(422, "invalid_decision", "decision must be waive_interview or require_interview");
|
|
3632
|
+
}
|
|
3633
|
+
const note = typeof body.note === "string" ? body.note.trim() : "";
|
|
3634
|
+
if (note.length > MAX_NOTE2) return reject4(422, "invalid_note", "note is too long");
|
|
3635
|
+
const expiresAt = body.expiresAt == null ? void 0 : Number(body.expiresAt);
|
|
3636
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= Date.now())) {
|
|
3637
|
+
return reject4(422, "invalid_expiry", "expiresAt must be a future epoch timestamp");
|
|
3638
|
+
}
|
|
3639
|
+
try {
|
|
3640
|
+
const saved = await persistLeaderAdmissionDesiredSet({
|
|
3641
|
+
db,
|
|
3642
|
+
ctx,
|
|
3643
|
+
env,
|
|
3644
|
+
target,
|
|
3645
|
+
personId,
|
|
3646
|
+
decision,
|
|
3647
|
+
...note ? { note } : {},
|
|
3648
|
+
...expiresAt !== void 0 ? { expiresAt } : {},
|
|
3649
|
+
active: url.pathname !== REVOKE_PATH,
|
|
3650
|
+
mutationId,
|
|
3651
|
+
actor: auth.user.email ?? auth.user.userId
|
|
3652
|
+
});
|
|
3653
|
+
const delivery = await deliverSaved(db, env, ctx, target, saved.delivery);
|
|
3654
|
+
return json({ ok: true, duplicate: saved.duplicate, delivery }, delivery.status === "acknowledged" ? 200 : 202);
|
|
3655
|
+
} catch (error) {
|
|
3656
|
+
if (error instanceof Error && error.message === "mutation_conflict") {
|
|
3657
|
+
return reject4(409, "mutation_conflict", "mutationId was already used for another request");
|
|
3658
|
+
}
|
|
3659
|
+
if (error instanceof Error && /valid authoritative email/.test(error.message)) {
|
|
3660
|
+
return reject4(422, "invalid_email", error.message);
|
|
3661
|
+
}
|
|
3662
|
+
return reject4(409, "revision_conflict", "admission assignments changed; reload and retry");
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
async function retryLeaderAdmissionDeliveries(env, ctx, limit = 100) {
|
|
3666
|
+
const db = ctx.makeDb(env);
|
|
3667
|
+
const result = await db.query({ leaderAdmissionDeliveries: { $: { limit } } });
|
|
3668
|
+
const pending = admissionRows(result.leaderAdmissionDeliveries).filter((delivery) => delivery.status !== "acknowledged" && delivery.status !== "rejected").slice(0, Math.max(1, Math.min(limit, 500)));
|
|
3669
|
+
const delivered = [];
|
|
3670
|
+
for (const item of pending) {
|
|
3671
|
+
const target = targetFor(ctx, item.targetId);
|
|
3672
|
+
if (target) delivered.push(await deliverSaved(db, env, ctx, target, item));
|
|
3673
|
+
}
|
|
3674
|
+
return delivered;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3287
3677
|
// src/worker-routes-schedule.ts
|
|
3288
3678
|
var import_calendar = require("@odla-ai/calendar");
|
|
3289
3679
|
function errCode(err) {
|
|
@@ -4545,16 +4935,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
4545
4935
|
});
|
|
4546
4936
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
4547
4937
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
4548
|
-
const
|
|
4549
|
-
if (
|
|
4550
|
-
const result = await backfillCrm(crmDeps2(
|
|
4938
|
+
const gate10 = await adminGate(req, env, ctx);
|
|
4939
|
+
if (gate10 instanceof Response) return gate10;
|
|
4940
|
+
const result = await backfillCrm(crmDeps2(gate10.db, ctx));
|
|
4551
4941
|
return json({ ok: true, ...result });
|
|
4552
4942
|
};
|
|
4553
4943
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
4554
4944
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
4555
|
-
const
|
|
4556
|
-
if (
|
|
4557
|
-
const { db } =
|
|
4945
|
+
const gate10 = await adminGate(req, env, ctx);
|
|
4946
|
+
if (gate10 instanceof Response) return gate10;
|
|
4947
|
+
const { db } = gate10;
|
|
4558
4948
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
4559
4949
|
const [data, roleList] = await Promise.all([
|
|
4560
4950
|
db.query({
|
|
@@ -4607,9 +4997,9 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
4607
4997
|
};
|
|
4608
4998
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
4609
4999
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
4610
|
-
const
|
|
4611
|
-
if (
|
|
4612
|
-
const { db } =
|
|
5000
|
+
const gate10 = await adminGate(req, env, ctx);
|
|
5001
|
+
if (gate10 instanceof Response) return gate10;
|
|
5002
|
+
const { db } = gate10;
|
|
4613
5003
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
4614
5004
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
4615
5005
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -4620,9 +5010,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
4620
5010
|
};
|
|
4621
5011
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
4622
5012
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
4623
|
-
const
|
|
4624
|
-
if (
|
|
4625
|
-
const { db, actor } =
|
|
5013
|
+
const gate10 = await adminGate(req, env, ctx);
|
|
5014
|
+
if (gate10 instanceof Response) return gate10;
|
|
5015
|
+
const { db, actor } = gate10;
|
|
4626
5016
|
let body;
|
|
4627
5017
|
try {
|
|
4628
5018
|
body = await req.json();
|
|
@@ -4769,7 +5159,7 @@ function subAnnualCents(sub) {
|
|
|
4769
5159
|
}
|
|
4770
5160
|
|
|
4771
5161
|
// src/worker-routes-admin-dashboard.ts
|
|
4772
|
-
async function
|
|
5162
|
+
async function gate3(req, env, ctx) {
|
|
4773
5163
|
const rawDb = ctx.makeDb(env);
|
|
4774
5164
|
const u = await ctx.verifyUser(req, env);
|
|
4775
5165
|
if (!u) return json({ error: "unauthorized" }, 401);
|
|
@@ -4779,7 +5169,7 @@ async function gate2(req, env, ctx) {
|
|
|
4779
5169
|
var rows3 = (v) => Array.isArray(v) ? v : [];
|
|
4780
5170
|
var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
4781
5171
|
if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
|
|
4782
|
-
const got = await
|
|
5172
|
+
const got = await gate3(req, env, ctx);
|
|
4783
5173
|
if (got instanceof Response) return got;
|
|
4784
5174
|
const db = got;
|
|
4785
5175
|
const now = Date.now();
|
|
@@ -4862,7 +5252,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4862
5252
|
};
|
|
4863
5253
|
var handleAdminBilling = async (req, url, env, ctx) => {
|
|
4864
5254
|
if (req.method !== "GET" || url.pathname !== "/api/admin/billing") return null;
|
|
4865
|
-
const got = await
|
|
5255
|
+
const got = await gate3(req, env, ctx);
|
|
4866
5256
|
if (got instanceof Response) return got;
|
|
4867
5257
|
const db = got;
|
|
4868
5258
|
const sk = await getVaultSecret(db, "stripe_secret_key");
|
|
@@ -4922,7 +5312,7 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
4922
5312
|
|
|
4923
5313
|
// src/worker-routes-admin-lifecycle.ts
|
|
4924
5314
|
var import_calendar3 = require("@odla-ai/calendar");
|
|
4925
|
-
async function
|
|
5315
|
+
async function gate4(req, env, ctx) {
|
|
4926
5316
|
const rawDb = ctx.makeDb(env);
|
|
4927
5317
|
const u = await ctx.verifyUser(req, env);
|
|
4928
5318
|
if (!u) return json({ error: "unauthorized" }, 401);
|
|
@@ -4948,7 +5338,7 @@ async function loadGroup(db, id) {
|
|
|
4948
5338
|
var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
|
|
4949
5339
|
const m = url.pathname.match(/^\/api\/admin\/meetings\/([0-9a-f-]+)\/reschedule$/);
|
|
4950
5340
|
if (req.method !== "POST" || !m) return null;
|
|
4951
|
-
const got = await
|
|
5341
|
+
const got = await gate4(req, env, ctx);
|
|
4952
5342
|
if (got instanceof Response) return got;
|
|
4953
5343
|
const db = got;
|
|
4954
5344
|
const body = await readJson(req);
|
|
@@ -4989,7 +5379,7 @@ var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
|
|
|
4989
5379
|
var handleAdminMeetingCancel = async (req, url, env, ctx) => {
|
|
4990
5380
|
const m = url.pathname.match(/^\/api\/admin\/meetings\/([0-9a-f-]+)\/cancel$/);
|
|
4991
5381
|
if (req.method !== "POST" || !m) return null;
|
|
4992
|
-
const got = await
|
|
5382
|
+
const got = await gate4(req, env, ctx);
|
|
4993
5383
|
if (got instanceof Response) return got;
|
|
4994
5384
|
const db = got;
|
|
4995
5385
|
const { meetings } = await db.query({ meetings: { $: { where: { id: m[1] }, limit: 1 } } });
|
|
@@ -5012,7 +5402,7 @@ var handleAdminMeetingCancel = async (req, url, env, ctx) => {
|
|
|
5012
5402
|
var handleAdminApprove = async (req, url, env, ctx) => {
|
|
5013
5403
|
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/approve$/);
|
|
5014
5404
|
if (req.method !== "POST" || !m) return null;
|
|
5015
|
-
const got = await
|
|
5405
|
+
const got = await gate4(req, env, ctx);
|
|
5016
5406
|
if (got instanceof Response) return got;
|
|
5017
5407
|
const db = got;
|
|
5018
5408
|
const id = m[1];
|
|
@@ -5025,7 +5415,7 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
5025
5415
|
var handleAdminRefund = async (req, url, env, ctx) => {
|
|
5026
5416
|
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/refund$/);
|
|
5027
5417
|
if (req.method !== "POST" || !m) return null;
|
|
5028
|
-
const got = await
|
|
5418
|
+
const got = await gate4(req, env, ctx);
|
|
5029
5419
|
if (got instanceof Response) return got;
|
|
5030
5420
|
const db = got;
|
|
5031
5421
|
const app = await loadApp(db, m[1]);
|
|
@@ -5057,7 +5447,7 @@ var handleAdminRefund = async (req, url, env, ctx) => {
|
|
|
5057
5447
|
var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
5058
5448
|
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)$/);
|
|
5059
5449
|
if (req.method !== "PATCH" || !m) return null;
|
|
5060
|
-
const got = await
|
|
5450
|
+
const got = await gate4(req, env, ctx);
|
|
5061
5451
|
if (got instanceof Response) return got;
|
|
5062
5452
|
const db = got;
|
|
5063
5453
|
const id = m[1];
|
|
@@ -5085,7 +5475,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
5085
5475
|
};
|
|
5086
5476
|
|
|
5087
5477
|
// src/worker-routes-admin-comms.ts
|
|
5088
|
-
async function
|
|
5478
|
+
async function gate5(req, env, ctx) {
|
|
5089
5479
|
const rawDb = ctx.makeDb(env);
|
|
5090
5480
|
const u = await ctx.verifyUser(req, env);
|
|
5091
5481
|
if (!u) return json({ error: "unauthorized" }, 401);
|
|
@@ -5107,7 +5497,7 @@ var notifyDeps = (db, env) => ({
|
|
|
5107
5497
|
});
|
|
5108
5498
|
var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
5109
5499
|
if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
5110
|
-
const got = await
|
|
5500
|
+
const got = await gate5(req, env, ctx);
|
|
5111
5501
|
if (got instanceof Response) return got;
|
|
5112
5502
|
const db = got;
|
|
5113
5503
|
const group = await loadGroup2(db, ctx.chapter.id);
|
|
@@ -5169,7 +5559,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
5169
5559
|
};
|
|
5170
5560
|
var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
5171
5561
|
if (req.method !== "GET" || url.pathname !== "/api/admin/email/log") return null;
|
|
5172
|
-
const got = await
|
|
5562
|
+
const got = await gate5(req, env, ctx);
|
|
5173
5563
|
if (got instanceof Response) return got;
|
|
5174
5564
|
const { emailLog } = await got.query({ emailLog: { $: { order: { sentAt: "desc" }, limit: 50 } } });
|
|
5175
5565
|
const sends = (emailLog ?? []).map((r) => ({
|
|
@@ -5186,7 +5576,7 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
|
5186
5576
|
};
|
|
5187
5577
|
var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
5188
5578
|
if (req.method !== "POST" || url.pathname !== "/api/admin/email/test") return null;
|
|
5189
|
-
const got = await
|
|
5579
|
+
const got = await gate5(req, env, ctx);
|
|
5190
5580
|
if (got instanceof Response) return got;
|
|
5191
5581
|
const db = got;
|
|
5192
5582
|
let body;
|
|
@@ -5214,7 +5604,7 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
|
5214
5604
|
var handleAdminComms = async (req, url, env, ctx) => {
|
|
5215
5605
|
const m = url.pathname.match(/^\/api\/admin\/people\/([0-9a-fA-F-]+)\/comms$/);
|
|
5216
5606
|
if (req.method !== "GET" || !m) return null;
|
|
5217
|
-
const got = await
|
|
5607
|
+
const got = await gate5(req, env, ctx);
|
|
5218
5608
|
if (got instanceof Response) return got;
|
|
5219
5609
|
const db = got;
|
|
5220
5610
|
const appId = m[1];
|
|
@@ -5252,26 +5642,12 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
5252
5642
|
};
|
|
5253
5643
|
|
|
5254
5644
|
// src/worker-routes-network.ts
|
|
5255
|
-
var
|
|
5256
|
-
var
|
|
5257
|
-
|
|
5258
|
-
// src/worker-network-fetch.ts
|
|
5259
|
-
function targetFetcher(env, target) {
|
|
5260
|
-
if (!target.binding) return null;
|
|
5261
|
-
const candidate = env[target.binding];
|
|
5262
|
-
if (typeof candidate !== "object" || candidate === null || !("fetch" in candidate) || typeof candidate.fetch !== "function") {
|
|
5263
|
-
throw new Error(`service binding "${target.binding}" is unavailable`);
|
|
5264
|
-
}
|
|
5265
|
-
return candidate;
|
|
5266
|
-
}
|
|
5267
|
-
function fetchNetworkTarget(env, target, destination, init) {
|
|
5268
|
-
const binding = targetFetcher(env, target);
|
|
5269
|
-
return binding ? binding.fetch(new Request(destination, init)) : globalThis.fetch(destination, init);
|
|
5270
|
-
}
|
|
5645
|
+
var import_crm8 = require("@odla-ai/crm");
|
|
5646
|
+
var import_db8 = require("@odla-ai/db");
|
|
5271
5647
|
|
|
5272
5648
|
// src/worker-network-push.ts
|
|
5273
|
-
var
|
|
5274
|
-
var
|
|
5649
|
+
var import_crm7 = require("@odla-ai/crm");
|
|
5650
|
+
var import_db7 = require("@odla-ai/db");
|
|
5275
5651
|
async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
5276
5652
|
const secret = await getVaultSecret(db, target.secretName);
|
|
5277
5653
|
const crmDeps3 = {
|
|
@@ -5282,7 +5658,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
5282
5658
|
};
|
|
5283
5659
|
if (!secret) {
|
|
5284
5660
|
const error = `vault secret "${target.secretName}" is missing`;
|
|
5285
|
-
await (0,
|
|
5661
|
+
await (0, import_crm7.recordDeliveryAttempt)(crmDeps3, {
|
|
5286
5662
|
recordId: record4.id,
|
|
5287
5663
|
targetId: target.id,
|
|
5288
5664
|
status: "failed",
|
|
@@ -5305,7 +5681,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
5305
5681
|
const payloadBody = JSON.stringify(payload);
|
|
5306
5682
|
const destination = new URL("/api/network/shared", target.url);
|
|
5307
5683
|
try {
|
|
5308
|
-
const signed = await (0,
|
|
5684
|
+
const signed = await (0, import_db7.signFederatedRequest)({
|
|
5309
5685
|
secret,
|
|
5310
5686
|
sender: ctx.chapter.id,
|
|
5311
5687
|
method: "POST",
|
|
@@ -5321,7 +5697,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
5321
5697
|
const responseBody = await res.json().catch(() => ({}));
|
|
5322
5698
|
if (!res.ok) {
|
|
5323
5699
|
const error = responseBody.error ?? "follower rejected the record";
|
|
5324
|
-
await (0,
|
|
5700
|
+
await (0, import_crm7.recordDeliveryAttempt)(crmDeps3, {
|
|
5325
5701
|
recordId: record4.id,
|
|
5326
5702
|
targetId: target.id,
|
|
5327
5703
|
status: "failed",
|
|
@@ -5336,7 +5712,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
5336
5712
|
error
|
|
5337
5713
|
};
|
|
5338
5714
|
}
|
|
5339
|
-
await (0,
|
|
5715
|
+
await (0, import_crm7.recordDeliveryAttempt)(crmDeps3, {
|
|
5340
5716
|
recordId: record4.id,
|
|
5341
5717
|
targetId: target.id,
|
|
5342
5718
|
status: "delivered",
|
|
@@ -5352,7 +5728,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
5352
5728
|
};
|
|
5353
5729
|
} catch (err) {
|
|
5354
5730
|
const error = err instanceof Error ? err.message : "delivery failed";
|
|
5355
|
-
await (0,
|
|
5731
|
+
await (0, import_crm7.recordDeliveryAttempt)(crmDeps3, {
|
|
5356
5732
|
recordId: record4.id,
|
|
5357
5733
|
targetId: target.id,
|
|
5358
5734
|
status: "failed",
|
|
@@ -5364,7 +5740,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
5364
5740
|
}
|
|
5365
5741
|
|
|
5366
5742
|
// src/worker-routes-network.ts
|
|
5367
|
-
async function
|
|
5743
|
+
async function gate6(req, env, ctx) {
|
|
5368
5744
|
const db = ctx.makeDb(env);
|
|
5369
5745
|
const user = await ctx.verifyUser(req, env);
|
|
5370
5746
|
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
@@ -5373,7 +5749,7 @@ async function gate5(req, env, ctx) {
|
|
|
5373
5749
|
}
|
|
5374
5750
|
var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
5375
5751
|
if (req.method !== "GET" || url.pathname !== "/api/admin/network/targets") return null;
|
|
5376
|
-
const got = await
|
|
5752
|
+
const got = await gate6(req, env, ctx);
|
|
5377
5753
|
if ("response" in got) return got.response;
|
|
5378
5754
|
return json({
|
|
5379
5755
|
targets: ctx.chapter.network.targets.map(({
|
|
@@ -5400,31 +5776,55 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
|
|
|
5400
5776
|
const db = ctx.makeDb(env);
|
|
5401
5777
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
5402
5778
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
5403
|
-
const verified = await (0,
|
|
5779
|
+
const verified = await (0, import_db8.verifyFederatedRequest)(req, { secret });
|
|
5404
5780
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
5405
5781
|
const types = await Promise.all(
|
|
5406
|
-
Object.keys(ctx.chapter.crm.config.types).map((type) => (0,
|
|
5782
|
+
Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm8.typeSummary)({ crm: ctx.chapter.crm, db }, type))
|
|
5407
5783
|
);
|
|
5784
|
+
const activeConfiguration = ctx.chapter.signupControl ? await loadActiveSignupControl(db, ctx.chapter.id, env.ODLA_ENV) : null;
|
|
5408
5785
|
const snapshot = {
|
|
5409
5786
|
version: 1,
|
|
5410
5787
|
site: { id: ctx.chapter.id, name: ctx.chapter.name, mode: ctx.chapter.mode },
|
|
5411
5788
|
generatedAt: Date.now(),
|
|
5789
|
+
configuration: {
|
|
5790
|
+
activeRevision: activeConfiguration?.revision ?? null,
|
|
5791
|
+
...activeConfiguration ? { digest: activeConfiguration.digest } : {}
|
|
5792
|
+
},
|
|
5412
5793
|
types
|
|
5413
5794
|
};
|
|
5414
5795
|
return json(snapshot);
|
|
5415
5796
|
};
|
|
5416
5797
|
async function snapshotOne(db, env, ctx, target) {
|
|
5798
|
+
const transportKind = target.binding ? "binding" : "url";
|
|
5799
|
+
const base = {
|
|
5800
|
+
id: target.id,
|
|
5801
|
+
name: target.name,
|
|
5802
|
+
url: target.url,
|
|
5803
|
+
configuration: { activeRevision: null },
|
|
5804
|
+
publicSite: { available: null }
|
|
5805
|
+
};
|
|
5417
5806
|
const secret = await getVaultSecret(db, target.secretName);
|
|
5418
|
-
if (!secret) return {
|
|
5807
|
+
if (!secret) return {
|
|
5808
|
+
...base,
|
|
5809
|
+
available: false,
|
|
5810
|
+
transport: {
|
|
5811
|
+
kind: transportKind,
|
|
5812
|
+
...target.binding ? { binding: target.binding } : {},
|
|
5813
|
+
reachable: null,
|
|
5814
|
+
error: { code: "edge_secret_missing", message: "edge secret is missing" }
|
|
5815
|
+
},
|
|
5816
|
+
error: "edge secret is missing"
|
|
5817
|
+
};
|
|
5419
5818
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
5420
5819
|
try {
|
|
5421
|
-
const
|
|
5820
|
+
const transport = resolveNetworkTargetTransport(env, target);
|
|
5821
|
+
const headers = await (0, import_db8.signFederatedRequest)({
|
|
5422
5822
|
secret,
|
|
5423
5823
|
sender: ctx.chapter.id,
|
|
5424
5824
|
method: "GET",
|
|
5425
5825
|
url: destination
|
|
5426
5826
|
});
|
|
5427
|
-
const response = await
|
|
5827
|
+
const response = await transport.fetch(destination, {
|
|
5428
5828
|
headers,
|
|
5429
5829
|
signal: AbortSignal.timeout(1e4)
|
|
5430
5830
|
});
|
|
@@ -5432,34 +5832,53 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
5432
5832
|
if (!response.ok || !body || !("version" in body) || body.version !== 1 || !("site" in body) || body.site.id !== target.id || !Array.isArray(body.types)) {
|
|
5433
5833
|
const upstream = body && "error" in body && typeof body.error === "string" ? body.error : void 0;
|
|
5434
5834
|
return {
|
|
5435
|
-
|
|
5436
|
-
name: target.name,
|
|
5437
|
-
url: target.url,
|
|
5835
|
+
...base,
|
|
5438
5836
|
available: false,
|
|
5837
|
+
transport: {
|
|
5838
|
+
kind: transport.kind,
|
|
5839
|
+
...transport.binding ? { binding: transport.binding } : {},
|
|
5840
|
+
reachable: true
|
|
5841
|
+
},
|
|
5842
|
+
publicSite: { available: transport.kind === "url" ? true : null },
|
|
5439
5843
|
error: upstream ?? (response.ok ? "invalid follower snapshot" : `follower returned ${response.status}`)
|
|
5440
5844
|
};
|
|
5441
5845
|
}
|
|
5442
5846
|
return {
|
|
5443
|
-
|
|
5444
|
-
name: target.name,
|
|
5445
|
-
url: target.url,
|
|
5847
|
+
...base,
|
|
5446
5848
|
available: true,
|
|
5849
|
+
transport: {
|
|
5850
|
+
kind: transport.kind,
|
|
5851
|
+
...transport.binding ? { binding: transport.binding } : {},
|
|
5852
|
+
reachable: true
|
|
5853
|
+
},
|
|
5854
|
+
configuration: body.configuration ?? { activeRevision: null },
|
|
5855
|
+
publicSite: { available: transport.kind === "url" ? true : null },
|
|
5447
5856
|
generatedAt: body.generatedAt,
|
|
5448
5857
|
types: body.types
|
|
5449
5858
|
};
|
|
5450
5859
|
} catch (error) {
|
|
5860
|
+
const bindingError = error instanceof NetworkTargetTransportError;
|
|
5861
|
+
const message = error instanceof Error ? error.message : "snapshot failed";
|
|
5451
5862
|
return {
|
|
5452
|
-
|
|
5453
|
-
name: target.name,
|
|
5454
|
-
url: target.url,
|
|
5863
|
+
...base,
|
|
5455
5864
|
available: false,
|
|
5456
|
-
|
|
5865
|
+
transport: {
|
|
5866
|
+
kind: transportKind,
|
|
5867
|
+
...target.binding ? { binding: target.binding } : {},
|
|
5868
|
+
reachable: false,
|
|
5869
|
+
error: {
|
|
5870
|
+
code: bindingError ? "binding_unavailable" : target.binding ? "binding_request_failed" : "url_request_failed",
|
|
5871
|
+
message
|
|
5872
|
+
}
|
|
5873
|
+
},
|
|
5874
|
+
publicSite: { available: target.binding ? null : false },
|
|
5875
|
+
error: message
|
|
5457
5876
|
};
|
|
5458
5877
|
}
|
|
5459
5878
|
}
|
|
5460
5879
|
var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
5461
5880
|
if (req.method !== "GET" || url.pathname !== "/api/admin/network/rollup") return null;
|
|
5462
|
-
const got = await
|
|
5881
|
+
const got = await gate6(req, env, ctx);
|
|
5463
5882
|
if ("response" in got) return got.response;
|
|
5464
5883
|
const targets = await Promise.all(
|
|
5465
5884
|
ctx.chapter.network.targets.map((target) => snapshotOne(got.db, env, ctx, target))
|
|
@@ -5486,7 +5905,7 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
|
5486
5905
|
};
|
|
5487
5906
|
var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
5488
5907
|
if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
|
|
5489
|
-
const got = await
|
|
5908
|
+
const got = await gate6(req, env, ctx);
|
|
5490
5909
|
if ("response" in got) return got.response;
|
|
5491
5910
|
let body;
|
|
5492
5911
|
try {
|
|
@@ -5501,7 +5920,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
5501
5920
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
5502
5921
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
5503
5922
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
5504
|
-
const record4 = await (0,
|
|
5923
|
+
const record4 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
5505
5924
|
if (!record4) return json({ error: "record not found" }, 404);
|
|
5506
5925
|
const results = await Promise.all(
|
|
5507
5926
|
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
|
|
@@ -5510,8 +5929,8 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
5510
5929
|
};
|
|
5511
5930
|
|
|
5512
5931
|
// src/worker-routes-network-records-follower.ts
|
|
5513
|
-
var
|
|
5514
|
-
var
|
|
5932
|
+
var import_crm9 = require("@odla-ai/crm");
|
|
5933
|
+
var import_db9 = require("@odla-ai/db");
|
|
5515
5934
|
|
|
5516
5935
|
// src/worker-routes-network-records-common.ts
|
|
5517
5936
|
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
@@ -5520,7 +5939,7 @@ var MAX_OFFSET = 1e4;
|
|
|
5520
5939
|
var MAX_SEARCH = 200;
|
|
5521
5940
|
var MAX_BODY = 32768;
|
|
5522
5941
|
var RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,191}$/;
|
|
5523
|
-
var
|
|
5942
|
+
var MUTATION_ID2 = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{7,127}$/;
|
|
5524
5943
|
function bodyTooLarge(body) {
|
|
5525
5944
|
return new TextEncoder().encode(body).byteLength > MAX_BODY;
|
|
5526
5945
|
}
|
|
@@ -5594,16 +6013,16 @@ function mutationError(error) {
|
|
|
5594
6013
|
if (error?.code === "transact_guard_failed") {
|
|
5595
6014
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
5596
6015
|
}
|
|
5597
|
-
if (error instanceof
|
|
6016
|
+
if (error instanceof import_crm9.CrmInputError) {
|
|
5598
6017
|
return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
|
|
5599
6018
|
}
|
|
5600
6019
|
return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
|
|
5601
6020
|
}
|
|
5602
6021
|
async function priorMutation(db, id) {
|
|
5603
6022
|
const result = await db.query({
|
|
5604
|
-
[
|
|
6023
|
+
[import_crm9.CRM_NS.activity]: { $: { where: { id }, limit: 1 } }
|
|
5605
6024
|
});
|
|
5606
|
-
const row = (result[
|
|
6025
|
+
const row = (result[import_crm9.CRM_NS.activity] ?? [])[0];
|
|
5607
6026
|
const meta = row?.meta;
|
|
5608
6027
|
const receipt = meta?.[NETWORK_MUTATION_META];
|
|
5609
6028
|
return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint } : null;
|
|
@@ -5617,7 +6036,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5617
6036
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
5618
6037
|
const readers = ctx.chapter.network.readers;
|
|
5619
6038
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
5620
|
-
const verified = await (0,
|
|
6039
|
+
const verified = await (0, import_db9.verifyFederatedRequest)(req, {
|
|
5621
6040
|
secret,
|
|
5622
6041
|
senders: readers.map((reader2) => reader2.id)
|
|
5623
6042
|
});
|
|
@@ -5638,7 +6057,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5638
6057
|
const hasFields = Boolean(fields && typeof fields === "object" && !Array.isArray(fields));
|
|
5639
6058
|
const hasStage = typeof input?.stage === "string";
|
|
5640
6059
|
const actor = input?.actor;
|
|
5641
|
-
if (!input || !IDENTIFIER.test(input.type) || !RECORD_ID.test(input.recordId) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || !
|
|
6060
|
+
if (!input || !IDENTIFIER.test(input.type) || !RECORD_ID.test(input.recordId) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || !MUTATION_ID2.test(input.mutationId) || !actor || typeof actor.id !== "string" || !RECORD_ID.test(actor.id) || hasFields === hasStage) {
|
|
5642
6061
|
return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
|
|
5643
6062
|
}
|
|
5644
6063
|
const editable = reader.editableFields[input.type];
|
|
@@ -5657,7 +6076,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5657
6076
|
if (prior.fingerprint !== fingerprint) {
|
|
5658
6077
|
return json({ error: "mutationId was already used for different input", code: "mutation_conflict" }, 409);
|
|
5659
6078
|
}
|
|
5660
|
-
const replayed = await (0,
|
|
6079
|
+
const replayed = await (0, import_crm9.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
5661
6080
|
if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
5662
6081
|
const result = {
|
|
5663
6082
|
version: 1,
|
|
@@ -5667,7 +6086,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5667
6086
|
};
|
|
5668
6087
|
return json(result);
|
|
5669
6088
|
}
|
|
5670
|
-
const record4 = await (0,
|
|
6089
|
+
const record4 = await (0, import_crm9.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
5671
6090
|
if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
5672
6091
|
if (record4.updatedAt !== input.expectedRevision) {
|
|
5673
6092
|
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
@@ -5688,7 +6107,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5688
6107
|
};
|
|
5689
6108
|
try {
|
|
5690
6109
|
const deps = { crm: ctx.chapter.crm, db };
|
|
5691
|
-
const updated = hasFields ? await (0,
|
|
6110
|
+
const updated = hasFields ? await (0, import_crm9.updateRecord)(deps, {
|
|
5692
6111
|
id: record4.id,
|
|
5693
6112
|
input: fields,
|
|
5694
6113
|
mutationId: activityId,
|
|
@@ -5699,7 +6118,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5699
6118
|
authorId: `network:${verified.sender}`,
|
|
5700
6119
|
meta: auditMeta
|
|
5701
6120
|
}
|
|
5702
|
-
}) : await (0,
|
|
6121
|
+
}) : await (0, import_crm9.setStage)(deps, {
|
|
5703
6122
|
id: record4.id,
|
|
5704
6123
|
to: input.stage,
|
|
5705
6124
|
authorId: `network:${verified.sender}`,
|
|
@@ -5729,7 +6148,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5729
6148
|
if (limit == null || offset == null || search.length > MAX_SEARCH) {
|
|
5730
6149
|
return json({ error: "invalid pagination or search parameters" }, 400);
|
|
5731
6150
|
}
|
|
5732
|
-
const page = await (0,
|
|
6151
|
+
const page = await (0, import_crm9.listRecords)(
|
|
5733
6152
|
{ crm: ctx.chapter.crm, db },
|
|
5734
6153
|
{
|
|
5735
6154
|
type,
|
|
@@ -5752,15 +6171,15 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5752
6171
|
};
|
|
5753
6172
|
|
|
5754
6173
|
// src/worker-routes-network-records-admin.ts
|
|
5755
|
-
var
|
|
5756
|
-
async function
|
|
6174
|
+
var import_db10 = require("@odla-ai/db");
|
|
6175
|
+
async function gate7(req, env, ctx) {
|
|
5757
6176
|
const db = ctx.makeDb(env);
|
|
5758
6177
|
const user = await ctx.verifyUser(req, env);
|
|
5759
6178
|
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
5760
6179
|
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
5761
6180
|
return { db, user };
|
|
5762
6181
|
}
|
|
5763
|
-
function
|
|
6182
|
+
function targetFor2(ctx, id) {
|
|
5764
6183
|
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
5765
6184
|
}
|
|
5766
6185
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
@@ -5769,7 +6188,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
5769
6188
|
const destination = new URL("/api/network/records", target.url);
|
|
5770
6189
|
destination.search = query.toString();
|
|
5771
6190
|
try {
|
|
5772
|
-
const headers = await (0,
|
|
6191
|
+
const headers = await (0, import_db10.signFederatedRequest)({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
|
|
5773
6192
|
const response = await fetchNetworkTarget(env, target, destination, {
|
|
5774
6193
|
headers,
|
|
5775
6194
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -5805,7 +6224,7 @@ async function mutateTargetRecord(db, env, ctx, target, input) {
|
|
|
5805
6224
|
const destination = new URL("/api/network/records", target.url);
|
|
5806
6225
|
const body = JSON.stringify(input);
|
|
5807
6226
|
try {
|
|
5808
|
-
const headers = await (0,
|
|
6227
|
+
const headers = await (0, import_db10.signFederatedRequest)({
|
|
5809
6228
|
secret,
|
|
5810
6229
|
sender: ctx.chapter.id,
|
|
5811
6230
|
method: "PATCH",
|
|
@@ -5837,7 +6256,7 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5837
6256
|
if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
|
|
5838
6257
|
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
5839
6258
|
}
|
|
5840
|
-
const got = await
|
|
6259
|
+
const got = await gate7(req, env, ctx);
|
|
5841
6260
|
if ("response" in got) return got.response;
|
|
5842
6261
|
if (req.method === "PATCH") {
|
|
5843
6262
|
let input;
|
|
@@ -5850,10 +6269,10 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5850
6269
|
} catch {
|
|
5851
6270
|
return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
|
|
5852
6271
|
}
|
|
5853
|
-
const target2 =
|
|
6272
|
+
const target2 = targetFor2(ctx, input?.targetId);
|
|
5854
6273
|
const hasFields = Boolean(input?.fields && typeof input.fields === "object" && !Array.isArray(input.fields));
|
|
5855
6274
|
const hasStage = typeof input?.stage === "string";
|
|
5856
|
-
if (!target2 || !IDENTIFIER.test(input.type) || !RECORD_ID.test(input.recordId) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || !
|
|
6275
|
+
if (!target2 || !IDENTIFIER.test(input.type) || !RECORD_ID.test(input.recordId) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || !MUTATION_ID2.test(input.mutationId) || hasFields === hasStage) {
|
|
5857
6276
|
return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
|
|
5858
6277
|
}
|
|
5859
6278
|
return mutateTargetRecord(got.db, env, ctx, target2, {
|
|
@@ -5868,7 +6287,7 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5868
6287
|
}
|
|
5869
6288
|
const targetId = url.searchParams.get("targetId") ?? "";
|
|
5870
6289
|
const type = url.searchParams.get("type") ?? "";
|
|
5871
|
-
const target =
|
|
6290
|
+
const target = targetFor2(ctx, targetId);
|
|
5872
6291
|
if (!target || !IDENTIFIER.test(type)) return json({ error: "configured targetId and record type are required" }, 400);
|
|
5873
6292
|
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
5874
6293
|
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
@@ -5885,8 +6304,8 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5885
6304
|
// src/worker-routes-network-notes.ts
|
|
5886
6305
|
var IDENTIFIER2 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
5887
6306
|
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
5888
|
-
var
|
|
5889
|
-
async function
|
|
6307
|
+
var MAX_NOTE3 = 4e3;
|
|
6308
|
+
async function gate8(req, env, ctx) {
|
|
5890
6309
|
const db = ctx.makeDb(env);
|
|
5891
6310
|
const user = await ctx.verifyUser(req, env);
|
|
5892
6311
|
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
@@ -5920,7 +6339,7 @@ function asNote(row) {
|
|
|
5920
6339
|
}
|
|
5921
6340
|
var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
5922
6341
|
if (url.pathname !== "/api/admin/network/notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
5923
|
-
const got = await
|
|
6342
|
+
const got = await gate8(req, env, ctx);
|
|
5924
6343
|
if ("response" in got) return got.response;
|
|
5925
6344
|
const identity = noteIdentity(url, ctx);
|
|
5926
6345
|
if (!identity) return json({ error: "valid targetId, type, and recordId are required" }, 400);
|
|
@@ -5937,7 +6356,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
5937
6356
|
return json({ notes: (result.networkNotes ?? []).map((row) => asNote(row)) });
|
|
5938
6357
|
}
|
|
5939
6358
|
const contentLength = Number(req.headers.get("content-length") ?? 0);
|
|
5940
|
-
if (contentLength >
|
|
6359
|
+
if (contentLength > MAX_NOTE3 * 2) return json({ error: "note is too large" }, 413);
|
|
5941
6360
|
let input;
|
|
5942
6361
|
try {
|
|
5943
6362
|
input = await req.json();
|
|
@@ -5945,7 +6364,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
5945
6364
|
return json({ error: "invalid JSON body" }, 400);
|
|
5946
6365
|
}
|
|
5947
6366
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
5948
|
-
if (!body || body.length >
|
|
6367
|
+
if (!body || body.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
|
|
5949
6368
|
const id = crypto.randomUUID();
|
|
5950
6369
|
const note = {
|
|
5951
6370
|
id,
|
|
@@ -5967,14 +6386,14 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
5967
6386
|
};
|
|
5968
6387
|
|
|
5969
6388
|
// src/worker-routes-network-shared-notes.ts
|
|
5970
|
-
var
|
|
5971
|
-
var
|
|
6389
|
+
var import_crm10 = require("@odla-ai/crm");
|
|
6390
|
+
var import_db11 = require("@odla-ai/db");
|
|
5972
6391
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
5973
6392
|
var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
5974
|
-
var
|
|
5975
|
-
var
|
|
6393
|
+
var MUTATION_ID3 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
6394
|
+
var MAX_NOTE4 = 4e3;
|
|
5976
6395
|
var NETWORK_META = "chapterNetwork";
|
|
5977
|
-
async function
|
|
6396
|
+
async function gate9(req, env, ctx) {
|
|
5978
6397
|
const db = ctx.makeDb(env);
|
|
5979
6398
|
const user = await ctx.verifyUser(req, env);
|
|
5980
6399
|
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
@@ -5997,7 +6416,7 @@ function sharedNote(activity, sourceId) {
|
|
|
5997
6416
|
}
|
|
5998
6417
|
async function listSharedNotes(db, sourceId, recordId) {
|
|
5999
6418
|
const result = await db.query({
|
|
6000
|
-
[
|
|
6419
|
+
[import_crm10.CRM_NS.activity]: {
|
|
6001
6420
|
$: {
|
|
6002
6421
|
where: {
|
|
6003
6422
|
and: [
|
|
@@ -6010,7 +6429,7 @@ async function listSharedNotes(db, sourceId, recordId) {
|
|
|
6010
6429
|
}
|
|
6011
6430
|
}
|
|
6012
6431
|
});
|
|
6013
|
-
return (result[
|
|
6432
|
+
return (result[import_crm10.CRM_NS.activity] ?? []).flatMap((activity) => {
|
|
6014
6433
|
const note = sharedNote(activity, sourceId);
|
|
6015
6434
|
return note ? [note] : [];
|
|
6016
6435
|
});
|
|
@@ -6020,14 +6439,14 @@ function sourceLabel(sourceId) {
|
|
|
6020
6439
|
}
|
|
6021
6440
|
var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
6022
6441
|
if (url.pathname !== "/api/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
6023
|
-
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) >
|
|
6442
|
+
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_NOTE4 * 2) {
|
|
6024
6443
|
return json({ error: "note is too large" }, 413);
|
|
6025
6444
|
}
|
|
6026
6445
|
const db = ctx.makeDb(env);
|
|
6027
6446
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
6028
6447
|
const readers = ctx.chapter.network.readers;
|
|
6029
6448
|
if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
|
|
6030
|
-
const verified = await (0,
|
|
6449
|
+
const verified = await (0, import_db11.verifyFederatedRequest)(req, {
|
|
6031
6450
|
secret,
|
|
6032
6451
|
senders: readers.map((reader2) => reader2.id)
|
|
6033
6452
|
});
|
|
@@ -6038,7 +6457,7 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6038
6457
|
if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
6039
6458
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
6040
6459
|
}
|
|
6041
|
-
const record4 = await (0,
|
|
6460
|
+
const record4 = await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
|
|
6042
6461
|
if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
|
|
6043
6462
|
if (req.method === "GET") {
|
|
6044
6463
|
return json({
|
|
@@ -6055,12 +6474,12 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6055
6474
|
}
|
|
6056
6475
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
6057
6476
|
const mutationId = typeof input.mutationId === "string" ? input.mutationId : "";
|
|
6058
|
-
if (!body || body.length >
|
|
6059
|
-
return json({ error: `body must be 1\u2013${
|
|
6477
|
+
if (!body || body.length > MAX_NOTE4 || !MUTATION_ID3.test(mutationId)) {
|
|
6478
|
+
return json({ error: `body must be 1\u2013${MAX_NOTE4} characters and mutationId must be valid` }, 400);
|
|
6060
6479
|
}
|
|
6061
6480
|
const createdAt = Date.now();
|
|
6062
6481
|
const id = `network:${verified.sender}:${mutationId}`;
|
|
6063
|
-
const result = await (0,
|
|
6482
|
+
const result = await (0, import_crm10.addActivity)(
|
|
6064
6483
|
{
|
|
6065
6484
|
crm: ctx.chapter.crm,
|
|
6066
6485
|
db,
|
|
@@ -6093,17 +6512,17 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6093
6512
|
};
|
|
6094
6513
|
return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
|
|
6095
6514
|
};
|
|
6096
|
-
function
|
|
6515
|
+
function targetFor3(ctx, id) {
|
|
6097
6516
|
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
6098
6517
|
}
|
|
6099
6518
|
var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
6100
6519
|
if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
6101
|
-
const got = await
|
|
6520
|
+
const got = await gate9(req, env, ctx);
|
|
6102
6521
|
if ("response" in got) return got.response;
|
|
6103
6522
|
const targetId = url.searchParams.get("targetId") ?? "";
|
|
6104
6523
|
const recordType = url.searchParams.get("type") ?? "";
|
|
6105
6524
|
const recordId = url.searchParams.get("recordId") ?? "";
|
|
6106
|
-
const target =
|
|
6525
|
+
const target = targetFor3(ctx, targetId);
|
|
6107
6526
|
if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId)) {
|
|
6108
6527
|
return json({ error: "configured targetId, type, and recordId are required" }, 400);
|
|
6109
6528
|
}
|
|
@@ -6116,7 +6535,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6116
6535
|
return json({ error: "invalid JSON body" }, 400);
|
|
6117
6536
|
}
|
|
6118
6537
|
const note = typeof input.body === "string" ? input.body.trim() : "";
|
|
6119
|
-
if (!note || note.length >
|
|
6538
|
+
if (!note || note.length > MAX_NOTE4) return json({ error: `note must be 1\u2013${MAX_NOTE4} characters` }, 400);
|
|
6120
6539
|
body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
|
|
6121
6540
|
}
|
|
6122
6541
|
const secret = await getVaultSecret(got.db, target.secretName);
|
|
@@ -6124,7 +6543,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6124
6543
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
6125
6544
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
6126
6545
|
try {
|
|
6127
|
-
const headers = await (0,
|
|
6546
|
+
const headers = await (0, import_db11.signFederatedRequest)({
|
|
6128
6547
|
secret,
|
|
6129
6548
|
sender: ctx.chapter.id,
|
|
6130
6549
|
method: req.method,
|
|
@@ -6151,7 +6570,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
6151
6570
|
};
|
|
6152
6571
|
|
|
6153
6572
|
// src/worker-routes-formation.ts
|
|
6154
|
-
var
|
|
6573
|
+
var import_crm11 = require("@odla-ai/crm");
|
|
6155
6574
|
var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
|
|
6156
6575
|
var handleFormation = async (req, url, env, ctx) => {
|
|
6157
6576
|
const formation = ctx.chapter.formation;
|
|
@@ -6203,7 +6622,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
6203
6622
|
const db = ctx.makeDb(env);
|
|
6204
6623
|
const id = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
|
|
6205
6624
|
try {
|
|
6206
|
-
const created = await (0,
|
|
6625
|
+
const created = await (0, import_crm11.createRecord)(
|
|
6207
6626
|
{
|
|
6208
6627
|
crm: ctx.chapter.crm,
|
|
6209
6628
|
db,
|
|
@@ -6231,7 +6650,7 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
6231
6650
|
};
|
|
6232
6651
|
|
|
6233
6652
|
// src/worker-routes-builder-library.ts
|
|
6234
|
-
var
|
|
6653
|
+
var import_db12 = require("@odla-ai/db");
|
|
6235
6654
|
|
|
6236
6655
|
// src/builder-library-validation.ts
|
|
6237
6656
|
var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -6514,7 +6933,7 @@ async function verifySource(req, db, ctx) {
|
|
|
6514
6933
|
if (ctx.chapter.mode === "chapter") {
|
|
6515
6934
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
6516
6935
|
if (!secret) return null;
|
|
6517
|
-
const verified = await (0,
|
|
6936
|
+
const verified = await (0, import_db12.verifyFederatedRequest)(req, {
|
|
6518
6937
|
secret,
|
|
6519
6938
|
senders: sources.map((source2) => source2.sourceId)
|
|
6520
6939
|
});
|
|
@@ -6527,7 +6946,7 @@ async function verifySource(req, db, ctx) {
|
|
|
6527
6946
|
if (!target) continue;
|
|
6528
6947
|
const secret = await getVaultSecret(db, target.secretName);
|
|
6529
6948
|
if (!secret) continue;
|
|
6530
|
-
const verified = await (0,
|
|
6949
|
+
const verified = await (0, import_db12.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
|
|
6531
6950
|
if (verified.ok) return { source, body: verified.body };
|
|
6532
6951
|
}
|
|
6533
6952
|
return null;
|
|
@@ -6716,11 +7135,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
6716
7135
|
};
|
|
6717
7136
|
|
|
6718
7137
|
// src/signup-control-delivery.ts
|
|
6719
|
-
var
|
|
7138
|
+
var import_db13 = require("@odla-ai/db");
|
|
6720
7139
|
async function deliverSignupControl(options) {
|
|
6721
7140
|
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
6722
7141
|
const body = JSON.stringify(options.envelope);
|
|
6723
|
-
const signed = await (0,
|
|
7142
|
+
const signed = await (0, import_db13.signFederatedRequest)({
|
|
6724
7143
|
secret: options.secret,
|
|
6725
7144
|
sender: options.sender,
|
|
6726
7145
|
method: "POST",
|
|
@@ -6741,11 +7160,11 @@ async function deliverSignupControl(options) {
|
|
|
6741
7160
|
}
|
|
6742
7161
|
|
|
6743
7162
|
// src/member-content-delivery.ts
|
|
6744
|
-
var
|
|
7163
|
+
var import_db14 = require("@odla-ai/db");
|
|
6745
7164
|
async function deliverMemberContent(options) {
|
|
6746
7165
|
const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
|
|
6747
7166
|
const body = JSON.stringify(options.envelope);
|
|
6748
|
-
const signed = await (0,
|
|
7167
|
+
const signed = await (0, import_db14.signFederatedRequest)({
|
|
6749
7168
|
secret: options.secret,
|
|
6750
7169
|
sender: options.sender,
|
|
6751
7170
|
method: "POST",
|
|
@@ -6766,11 +7185,11 @@ async function deliverMemberContent(options) {
|
|
|
6766
7185
|
}
|
|
6767
7186
|
|
|
6768
7187
|
// src/builder-library-delivery.ts
|
|
6769
|
-
var
|
|
7188
|
+
var import_db15 = require("@odla-ai/db");
|
|
6770
7189
|
async function deliverBuilderLibrary(options) {
|
|
6771
7190
|
const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
|
|
6772
7191
|
const body = JSON.stringify(options.envelope);
|
|
6773
|
-
const signed = await (0,
|
|
7192
|
+
const signed = await (0, import_db15.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
|
|
6774
7193
|
const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
|
|
6775
7194
|
const result = await response.json();
|
|
6776
7195
|
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
@@ -6780,31 +7199,6 @@ async function deliverBuilderLibrary(options) {
|
|
|
6780
7199
|
return result;
|
|
6781
7200
|
}
|
|
6782
7201
|
|
|
6783
|
-
// src/admission-grants-delivery.ts
|
|
6784
|
-
var import_db15 = require("@odla-ai/db");
|
|
6785
|
-
async function deliverAdmissionGrants(options) {
|
|
6786
|
-
const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
|
|
6787
|
-
const body = JSON.stringify(options.envelope);
|
|
6788
|
-
const signed = await (0, import_db15.signFederatedRequest)({
|
|
6789
|
-
secret: options.secret,
|
|
6790
|
-
sender: options.sender,
|
|
6791
|
-
method: "POST",
|
|
6792
|
-
url: destination,
|
|
6793
|
-
body
|
|
6794
|
-
});
|
|
6795
|
-
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
6796
|
-
method: "POST",
|
|
6797
|
-
headers: { ...signed, "content-type": "application/json" },
|
|
6798
|
-
body
|
|
6799
|
-
});
|
|
6800
|
-
const result = await response.json();
|
|
6801
|
-
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
6802
|
-
const failure = result;
|
|
6803
|
-
throw new Error(`admission grants rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
|
|
6804
|
-
}
|
|
6805
|
-
return result;
|
|
6806
|
-
}
|
|
6807
|
-
|
|
6808
7202
|
// src/signup-control-theme.ts
|
|
6809
7203
|
var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
|
|
6810
7204
|
var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
|
|
@@ -6964,6 +7358,7 @@ var BUILTIN_ROUTES = [
|
|
|
6964
7358
|
handleAdminPeopleRole,
|
|
6965
7359
|
handleAdminCrmSync,
|
|
6966
7360
|
handleAdminAdmissionGrants,
|
|
7361
|
+
handleAdminNetworkAdmissions,
|
|
6967
7362
|
handleAdminClerkPrivateProfileSync,
|
|
6968
7363
|
// Aggregation
|
|
6969
7364
|
handleAdminDashboard,
|