@odla-ai/chapter 0.42.0 → 0.42.2
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 +32 -4
- package/dist/{chunk-MMDWXTJU.js → chunk-HWUTCXBV.js} +44 -1
- package/dist/chunk-HWUTCXBV.js.map +1 -0
- package/dist/{copy-context-DuJkH4U9.d.ts → copy-context-BBPsYe24.d.ts} +27 -1
- package/dist/index.cjs +144 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -3
- package/dist/index.d.ts +75 -3
- package/dist/index.js +144 -16
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +14 -3
- package/dist/ui/admin/index.js +3 -1
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +3 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +745 -163
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +73 -2
- package/dist/worker/index.d.ts +73 -2
- package/dist/worker/index.js +730 -148
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/runbooks/greenfield.md +3 -0
- package/dist/chunk-MMDWXTJU.js.map +0 -1
package/dist/worker/index.js
CHANGED
|
@@ -607,6 +607,11 @@ function validateAdmissionGrantDocument(input) {
|
|
|
607
607
|
}
|
|
608
608
|
};
|
|
609
609
|
}
|
|
610
|
+
async function createAdmissionGrantEnvelope(document) {
|
|
611
|
+
const checked = validateAdmissionGrantDocument(document);
|
|
612
|
+
if (!checked.ok) throw new TypeError(`Invalid admission grant document: ${checked.errors.join("; ")}`);
|
|
613
|
+
return { document: checked.value, digest: await signupControlDigest(checked.value) };
|
|
614
|
+
}
|
|
610
615
|
var rows = (value) => Array.isArray(value) ? value : [];
|
|
611
616
|
function asGrant(row) {
|
|
612
617
|
if (typeof row.id !== "string" || typeof row.emailNormalized !== "string") return null;
|
|
@@ -3110,12 +3115,17 @@ import { verifyFederatedRequest as verifyFederatedRequest4 } from "@odla-ai/db";
|
|
|
3110
3115
|
var reject3 = (reason, status, errors) => json({ ok: false, reason, ...errors ? { errors } : {} }, status);
|
|
3111
3116
|
var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
3112
3117
|
if (req.method !== "POST" || url.pathname !== ADMISSION_GRANTS_PATH) return null;
|
|
3113
|
-
const
|
|
3114
|
-
|
|
3118
|
+
const claimedSender = req.headers.get("x-odla-federation-sender") ?? "";
|
|
3119
|
+
const networkReader = ctx.chapter.network.readers.find((reader) => reader.id === claimedSender && reader.admissionGrants);
|
|
3120
|
+
const signupAuthority = ctx.chapter.signupControl?.sourceId === claimedSender ? ctx.chapter.signupControl : void 0;
|
|
3121
|
+
if (!networkReader && !signupAuthority) return json({ error: "not found" }, 404);
|
|
3115
3122
|
const db = ctx.makeDb(env);
|
|
3116
|
-
const secret = await getVaultSecret(
|
|
3123
|
+
const secret = await getVaultSecret(
|
|
3124
|
+
db,
|
|
3125
|
+
networkReader ? "network_share_secret" : signupAuthority.secretName
|
|
3126
|
+
);
|
|
3117
3127
|
if (!secret) return json({ error: "unauthorized" }, 401);
|
|
3118
|
-
const verified = await verifyFederatedRequest4(req, { secret, senders: [
|
|
3128
|
+
const verified = await verifyFederatedRequest4(req, { secret, senders: [claimedSender] });
|
|
3119
3129
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
3120
3130
|
let input;
|
|
3121
3131
|
try {
|
|
@@ -3127,11 +3137,11 @@ var handleAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3127
3137
|
const checked = validateAdmissionGrantDocument(input.document);
|
|
3128
3138
|
if (!checked.ok) return reject3("invalid_document", 400, checked.errors);
|
|
3129
3139
|
const document = checked.value;
|
|
3130
|
-
if (document.source.appId !==
|
|
3140
|
+
if (document.source.appId !== claimedSender) return reject3("wrong_source", 403);
|
|
3131
3141
|
if (document.target.chapterId !== ctx.chapter.id) return reject3("wrong_target", 409);
|
|
3132
3142
|
if (document.target.environment !== env.ODLA_ENV) return reject3("wrong_environment", 409);
|
|
3133
3143
|
if (!digest || await signupControlDigest(document) !== digest) return reject3("invalid_digest", 400);
|
|
3134
|
-
const latest = await loadLatestAdmissionRevision(db,
|
|
3144
|
+
const latest = await loadLatestAdmissionRevision(db, claimedSender, ctx.chapter.id, env.ODLA_ENV);
|
|
3135
3145
|
if (latest && document.revision < latest.revision) return reject3("stale_revision", 409);
|
|
3136
3146
|
if (latest && document.revision === latest.revision) {
|
|
3137
3147
|
if (latest.digest !== digest) return reject3("conflicting_revision", 409);
|
|
@@ -3256,6 +3266,365 @@ var handleAdminAdmissionGrants = async (req, url, env, ctx) => {
|
|
|
3256
3266
|
return null;
|
|
3257
3267
|
};
|
|
3258
3268
|
|
|
3269
|
+
// src/worker-routes-network-admission.ts
|
|
3270
|
+
import { getRecord as getRecord2 } from "@odla-ai/crm";
|
|
3271
|
+
|
|
3272
|
+
// src/admission-grants-delivery.ts
|
|
3273
|
+
import { signFederatedRequest } from "@odla-ai/db";
|
|
3274
|
+
var AdmissionGrantDeliveryError = class extends Error {
|
|
3275
|
+
constructor(status, reason) {
|
|
3276
|
+
super(`admission grants rejected (${status}): ${reason}`);
|
|
3277
|
+
this.status = status;
|
|
3278
|
+
this.reason = reason;
|
|
3279
|
+
this.name = "AdmissionGrantDeliveryError";
|
|
3280
|
+
}
|
|
3281
|
+
status;
|
|
3282
|
+
reason;
|
|
3283
|
+
};
|
|
3284
|
+
async function deliverAdmissionGrants(options) {
|
|
3285
|
+
const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
|
|
3286
|
+
const body = JSON.stringify(options.envelope);
|
|
3287
|
+
const signed = await signFederatedRequest({
|
|
3288
|
+
secret: options.secret,
|
|
3289
|
+
sender: options.sender,
|
|
3290
|
+
method: "POST",
|
|
3291
|
+
url: destination,
|
|
3292
|
+
body
|
|
3293
|
+
});
|
|
3294
|
+
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
3295
|
+
method: "POST",
|
|
3296
|
+
headers: { ...signed, "content-type": "application/json" },
|
|
3297
|
+
body
|
|
3298
|
+
});
|
|
3299
|
+
const result = await response.json();
|
|
3300
|
+
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
3301
|
+
const failure = result;
|
|
3302
|
+
throw new AdmissionGrantDeliveryError(
|
|
3303
|
+
response.status,
|
|
3304
|
+
failure.reason ?? failure.error ?? "unknown"
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
return result;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
// src/worker-network-fetch.ts
|
|
3311
|
+
function targetFetcher(env, target) {
|
|
3312
|
+
if (!target.binding) return null;
|
|
3313
|
+
const candidate = env[target.binding];
|
|
3314
|
+
if (typeof candidate !== "object" || candidate === null || !("fetch" in candidate) || typeof candidate.fetch !== "function") {
|
|
3315
|
+
throw new Error(`service binding "${target.binding}" is unavailable`);
|
|
3316
|
+
}
|
|
3317
|
+
return candidate;
|
|
3318
|
+
}
|
|
3319
|
+
function fetchNetworkTarget(env, target, destination, init) {
|
|
3320
|
+
const binding = targetFetcher(env, target);
|
|
3321
|
+
return binding ? binding.fetch(new Request(destination, init)) : globalThis.fetch(destination, init);
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// src/worker-network-admission-state.ts
|
|
3325
|
+
import { getRecord } from "@odla-ai/crm";
|
|
3326
|
+
var MAX_ASSIGNMENTS = 2e3;
|
|
3327
|
+
var admissionRows = (value) => Array.isArray(value) ? value : [];
|
|
3328
|
+
function assignmentId(targetId, personId) {
|
|
3329
|
+
return `leader-admission:${targetId}:${personId}`;
|
|
3330
|
+
}
|
|
3331
|
+
function deliveryId(targetId, environment) {
|
|
3332
|
+
return `leader-admission-delivery:${targetId}:${environment}`;
|
|
3333
|
+
}
|
|
3334
|
+
async function loadLeaderAdmissionAssignments(db, targetId) {
|
|
3335
|
+
const result = await db.query({ leaderAdmissionAssignments: { $: {
|
|
3336
|
+
where: { targetId },
|
|
3337
|
+
order: { updatedAt: "desc" },
|
|
3338
|
+
limit: MAX_ASSIGNMENTS
|
|
3339
|
+
} } });
|
|
3340
|
+
return admissionRows(result.leaderAdmissionAssignments);
|
|
3341
|
+
}
|
|
3342
|
+
async function loadLeaderAdmissionDelivery(db, targetId, environment) {
|
|
3343
|
+
const id = deliveryId(targetId, environment);
|
|
3344
|
+
const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id }, limit: 1 } } });
|
|
3345
|
+
return admissionRows(result.leaderAdmissionDeliveries)[0] ?? null;
|
|
3346
|
+
}
|
|
3347
|
+
function personAdmissionEmail(ctx, record4) {
|
|
3348
|
+
if (record4.type !== "person") return null;
|
|
3349
|
+
const field = ctx.chapter.crm.config.types.person?.emailField;
|
|
3350
|
+
if (!field) return null;
|
|
3351
|
+
const raw = record4.fields[field] ?? record4.primaryEmail;
|
|
3352
|
+
if (typeof raw !== "string") return null;
|
|
3353
|
+
const normalized = normalizeAdmissionEmail(raw);
|
|
3354
|
+
return isValidEmail(normalized) ? normalized : null;
|
|
3355
|
+
}
|
|
3356
|
+
async function grantInput(db, ctx, assignment) {
|
|
3357
|
+
const person = await getRecord({ crm: ctx.chapter.crm, db }, assignment.personId);
|
|
3358
|
+
const email = person ? personAdmissionEmail(ctx, person) : null;
|
|
3359
|
+
if (!email) throw new Error(`person ${assignment.personId} has no valid authoritative email`);
|
|
3360
|
+
return {
|
|
3361
|
+
id: assignment.grantId,
|
|
3362
|
+
email,
|
|
3363
|
+
decision: assignment.decision,
|
|
3364
|
+
...assignment.note ? { note: assignment.note } : {},
|
|
3365
|
+
...assignment.expiresAt !== void 0 ? { expiresAt: assignment.expiresAt } : {}
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
async function persistLeaderAdmissionDesiredSet(opts) {
|
|
3369
|
+
const now = Date.now();
|
|
3370
|
+
const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
|
|
3371
|
+
const id = assignmentId(opts.target.id, opts.personId);
|
|
3372
|
+
const existing = current.find((row) => row.id === id);
|
|
3373
|
+
const digest = await signupControlDigest({
|
|
3374
|
+
targetId: opts.target.id,
|
|
3375
|
+
personId: opts.personId,
|
|
3376
|
+
decision: opts.decision,
|
|
3377
|
+
note: opts.note ?? null,
|
|
3378
|
+
expiresAt: opts.expiresAt ?? null,
|
|
3379
|
+
active: opts.active
|
|
3380
|
+
});
|
|
3381
|
+
const mutationReceiptId = `leader-admission-mutation:${opts.target.id}:${opts.mutationId}`;
|
|
3382
|
+
const priorMutation2 = await opts.db.query({ leaderAdmissionMutations: { $: {
|
|
3383
|
+
where: { id: mutationReceiptId },
|
|
3384
|
+
limit: 1
|
|
3385
|
+
} } });
|
|
3386
|
+
const receipt = admissionRows(priorMutation2.leaderAdmissionMutations)[0];
|
|
3387
|
+
if (receipt) {
|
|
3388
|
+
if (receipt.requestDigest !== digest || receipt.personId !== opts.personId) {
|
|
3389
|
+
throw new Error("mutation_conflict");
|
|
3390
|
+
}
|
|
3391
|
+
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3392
|
+
if (!saved) throw new Error("delivery_missing");
|
|
3393
|
+
return { delivery: saved, duplicate: true };
|
|
3394
|
+
}
|
|
3395
|
+
if (existing?.lastMutationId === opts.mutationId) {
|
|
3396
|
+
if (existing.lastRequestDigest !== digest) throw new Error("mutation_conflict");
|
|
3397
|
+
const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3398
|
+
if (!saved) throw new Error("delivery_missing");
|
|
3399
|
+
return { delivery: saved, duplicate: true };
|
|
3400
|
+
}
|
|
3401
|
+
const assignment = {
|
|
3402
|
+
id,
|
|
3403
|
+
targetId: opts.target.id,
|
|
3404
|
+
personId: opts.personId,
|
|
3405
|
+
grantId: existing?.grantId ?? crypto.randomUUID().replaceAll("-", ""),
|
|
3406
|
+
decision: opts.decision,
|
|
3407
|
+
...opts.note ? { note: opts.note } : {},
|
|
3408
|
+
...opts.expiresAt !== void 0 ? { expiresAt: opts.expiresAt } : {},
|
|
3409
|
+
active: opts.active,
|
|
3410
|
+
lastMutationId: opts.mutationId,
|
|
3411
|
+
lastRequestDigest: digest,
|
|
3412
|
+
createdAt: existing?.createdAt ?? now,
|
|
3413
|
+
createdBy: existing?.createdBy ?? opts.actor,
|
|
3414
|
+
updatedAt: now,
|
|
3415
|
+
updatedBy: opts.actor
|
|
3416
|
+
};
|
|
3417
|
+
const desired = current.filter((row) => row.id !== id && row.active).concat(assignment.active ? [assignment] : []);
|
|
3418
|
+
const grants = await Promise.all(desired.map((row) => grantInput(opts.db, opts.ctx, row)));
|
|
3419
|
+
const previous = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
|
|
3420
|
+
const revision = (previous?.revision ?? 0) + 1;
|
|
3421
|
+
const envelope = await createAdmissionGrantEnvelope({
|
|
3422
|
+
schemaVersion: 1,
|
|
3423
|
+
revision,
|
|
3424
|
+
generatedAt: now,
|
|
3425
|
+
source: { appId: opts.ctx.chapter.id, actor: opts.actor },
|
|
3426
|
+
target: { chapterId: opts.target.id, environment: opts.env.ODLA_ENV },
|
|
3427
|
+
grants
|
|
3428
|
+
});
|
|
3429
|
+
const delivery = {
|
|
3430
|
+
id: deliveryId(opts.target.id, opts.env.ODLA_ENV),
|
|
3431
|
+
targetId: opts.target.id,
|
|
3432
|
+
environment: opts.env.ODLA_ENV,
|
|
3433
|
+
revision,
|
|
3434
|
+
digest: envelope.digest,
|
|
3435
|
+
envelope,
|
|
3436
|
+
status: "saved",
|
|
3437
|
+
attempts: 0,
|
|
3438
|
+
updatedAt: now
|
|
3439
|
+
};
|
|
3440
|
+
const ops = [
|
|
3441
|
+
{ t: "update", ns: "leaderAdmissionAssignments", id, attrs: assignment },
|
|
3442
|
+
{ t: "update", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: delivery },
|
|
3443
|
+
{ t: "update", ns: "leaderAdmissionMutations", id: mutationReceiptId, attrs: {
|
|
3444
|
+
id: mutationReceiptId,
|
|
3445
|
+
targetId: opts.target.id,
|
|
3446
|
+
personId: opts.personId,
|
|
3447
|
+
requestDigest: digest,
|
|
3448
|
+
revision,
|
|
3449
|
+
createdAt: now
|
|
3450
|
+
} }
|
|
3451
|
+
];
|
|
3452
|
+
const retract = [
|
|
3453
|
+
...!opts.note && existing?.note !== void 0 ? ["note"] : [],
|
|
3454
|
+
...opts.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
|
|
3455
|
+
];
|
|
3456
|
+
if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id, attrs: retract });
|
|
3457
|
+
const tx = await opts.db.transact(ops, {
|
|
3458
|
+
mutationId: `leader-admission:${opts.target.id}:${opts.mutationId}`,
|
|
3459
|
+
guards: previous ? [{ ns: "leaderAdmissionDeliveries", id: previous.id, exists: true, equals: { revision: previous.revision } }] : [{ ns: "leaderAdmissionDeliveries", id: delivery.id, exists: false }]
|
|
3460
|
+
});
|
|
3461
|
+
return { delivery, duplicate: tx.duplicate };
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
// src/worker-routes-network-admission.ts
|
|
3465
|
+
var PATH = "/api/admin/network/admissions";
|
|
3466
|
+
var RETRY_PATH = "/api/admin/network/admissions/retry";
|
|
3467
|
+
var REVOKE_PATH = "/api/admin/network/admissions/revoke";
|
|
3468
|
+
var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
|
|
3469
|
+
var MAX_NOTE2 = 2e3;
|
|
3470
|
+
var reject4 = (status, code, error) => json({ ok: false, code, error }, status);
|
|
3471
|
+
async function gate2(req, env, ctx) {
|
|
3472
|
+
const db = ctx.makeDb(env);
|
|
3473
|
+
const user = await ctx.verifyUser(req, env);
|
|
3474
|
+
if (!user) return { response: reject4(401, "unauthorized", "unauthorized") };
|
|
3475
|
+
if (!await ctx.isAdmin(db, user)) return { response: reject4(403, "forbidden", "forbidden") };
|
|
3476
|
+
return { db, user };
|
|
3477
|
+
}
|
|
3478
|
+
function targetFor(ctx, id) {
|
|
3479
|
+
return ctx.chapter.network.targets.find((target) => target.id === id && target.admissionGrants);
|
|
3480
|
+
}
|
|
3481
|
+
async function deliverSaved(db, env, ctx, target, delivery) {
|
|
3482
|
+
if (delivery.status === "acknowledged") return delivery;
|
|
3483
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
3484
|
+
const attemptedAt = Date.now();
|
|
3485
|
+
const attempts = delivery.attempts + 1;
|
|
3486
|
+
let patch;
|
|
3487
|
+
if (!secret) {
|
|
3488
|
+
patch = {
|
|
3489
|
+
status: delivery.attempts > 0 ? "retrying" : "pending",
|
|
3490
|
+
attempts,
|
|
3491
|
+
lastAttemptAt: attemptedAt,
|
|
3492
|
+
lastError: "edge secret is missing",
|
|
3493
|
+
updatedAt: attemptedAt
|
|
3494
|
+
};
|
|
3495
|
+
} else {
|
|
3496
|
+
try {
|
|
3497
|
+
const receipt = await deliverAdmissionGrants({
|
|
3498
|
+
secret,
|
|
3499
|
+
sender: ctx.chapter.id,
|
|
3500
|
+
targetUrl: target.url,
|
|
3501
|
+
envelope: delivery.envelope,
|
|
3502
|
+
fetch: ((input, init) => fetchNetworkTarget(env, target, new URL(String(input)), init ?? {}))
|
|
3503
|
+
});
|
|
3504
|
+
patch = {
|
|
3505
|
+
status: "acknowledged",
|
|
3506
|
+
attempts,
|
|
3507
|
+
lastAttemptAt: attemptedAt,
|
|
3508
|
+
acknowledgedAt: receipt.acknowledgedAt,
|
|
3509
|
+
updatedAt: attemptedAt
|
|
3510
|
+
};
|
|
3511
|
+
} catch (error) {
|
|
3512
|
+
const permanent = error instanceof AdmissionGrantDeliveryError && error.status >= 400 && error.status < 500;
|
|
3513
|
+
patch = {
|
|
3514
|
+
status: permanent ? "rejected" : delivery.attempts > 0 ? "retrying" : "pending",
|
|
3515
|
+
attempts,
|
|
3516
|
+
lastAttemptAt: attemptedAt,
|
|
3517
|
+
lastError: error instanceof Error ? error.message.slice(0, 2e3) : "delivery failed",
|
|
3518
|
+
updatedAt: attemptedAt
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
const ops = [{
|
|
3523
|
+
t: "update",
|
|
3524
|
+
ns: "leaderAdmissionDeliveries",
|
|
3525
|
+
id: delivery.id,
|
|
3526
|
+
attrs: patch
|
|
3527
|
+
}];
|
|
3528
|
+
if (patch.status === "acknowledged" && delivery.lastError) {
|
|
3529
|
+
ops.push({ t: "retract", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: ["lastError"] });
|
|
3530
|
+
}
|
|
3531
|
+
await db.transact(ops, {
|
|
3532
|
+
mutationId: `leader-admission-attempt:${delivery.targetId}:${delivery.revision}:${attempts}:${patch.status}`
|
|
3533
|
+
});
|
|
3534
|
+
return { ...delivery, ...patch };
|
|
3535
|
+
}
|
|
3536
|
+
async function parseBody(req) {
|
|
3537
|
+
try {
|
|
3538
|
+
return await req.json();
|
|
3539
|
+
} catch {
|
|
3540
|
+
return null;
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
|
|
3544
|
+
if (![PATH, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
|
|
3545
|
+
const auth = await gate2(req, env, ctx);
|
|
3546
|
+
if ("response" in auth) return auth.response;
|
|
3547
|
+
const db = auth.db;
|
|
3548
|
+
if (url.pathname === PATH && req.method === "GET") {
|
|
3549
|
+
const targetId2 = url.searchParams.get("targetId") ?? "";
|
|
3550
|
+
const target2 = targetFor(ctx, targetId2);
|
|
3551
|
+
if (!target2) return reject4(404, "unknown_target", "admission target is not configured");
|
|
3552
|
+
return json({
|
|
3553
|
+
ok: true,
|
|
3554
|
+
target: { id: target2.id, name: target2.name },
|
|
3555
|
+
assignments: await loadLeaderAdmissionAssignments(db, target2.id),
|
|
3556
|
+
delivery: await loadLeaderAdmissionDelivery(db, target2.id, env.ODLA_ENV)
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
if (req.method !== "POST") return null;
|
|
3560
|
+
const body = await parseBody(req);
|
|
3561
|
+
if (!body) return reject4(422, "invalid_request", "request body must be JSON");
|
|
3562
|
+
const targetId = typeof body.targetId === "string" ? body.targetId : "";
|
|
3563
|
+
const target = targetFor(ctx, targetId);
|
|
3564
|
+
if (!target) return reject4(404, "unknown_target", "admission target is not configured");
|
|
3565
|
+
if (url.pathname === RETRY_PATH) {
|
|
3566
|
+
const delivery = await loadLeaderAdmissionDelivery(db, target.id, env.ODLA_ENV);
|
|
3567
|
+
if (!delivery) return reject4(404, "not_found", "no saved admission delivery exists");
|
|
3568
|
+
return json({ ok: true, delivery: await deliverSaved(db, env, ctx, target, delivery) });
|
|
3569
|
+
}
|
|
3570
|
+
const personId = typeof body.personId === "string" ? body.personId : "";
|
|
3571
|
+
const mutationId = typeof body.mutationId === "string" ? body.mutationId : "";
|
|
3572
|
+
if (!personId || !MUTATION_ID.test(mutationId)) {
|
|
3573
|
+
return reject4(422, "invalid_request", "personId and a stable mutationId are required");
|
|
3574
|
+
}
|
|
3575
|
+
const person = await getRecord2({ crm: ctx.chapter.crm, db: auth.db }, personId);
|
|
3576
|
+
if (!person) return reject4(404, "person_not_found", "person not found");
|
|
3577
|
+
if (!personAdmissionEmail(ctx, person)) {
|
|
3578
|
+
return reject4(422, "invalid_email", "person has no valid authoritative email");
|
|
3579
|
+
}
|
|
3580
|
+
const decision = body.decision ?? "waive_interview";
|
|
3581
|
+
if (decision !== "waive_interview" && decision !== "require_interview") {
|
|
3582
|
+
return reject4(422, "invalid_decision", "decision must be waive_interview or require_interview");
|
|
3583
|
+
}
|
|
3584
|
+
const note = typeof body.note === "string" ? body.note.trim() : "";
|
|
3585
|
+
if (note.length > MAX_NOTE2) return reject4(422, "invalid_note", "note is too long");
|
|
3586
|
+
const expiresAt = body.expiresAt == null ? void 0 : Number(body.expiresAt);
|
|
3587
|
+
if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= Date.now())) {
|
|
3588
|
+
return reject4(422, "invalid_expiry", "expiresAt must be a future epoch timestamp");
|
|
3589
|
+
}
|
|
3590
|
+
try {
|
|
3591
|
+
const saved = await persistLeaderAdmissionDesiredSet({
|
|
3592
|
+
db,
|
|
3593
|
+
ctx,
|
|
3594
|
+
env,
|
|
3595
|
+
target,
|
|
3596
|
+
personId,
|
|
3597
|
+
decision,
|
|
3598
|
+
...note ? { note } : {},
|
|
3599
|
+
...expiresAt !== void 0 ? { expiresAt } : {},
|
|
3600
|
+
active: url.pathname !== REVOKE_PATH,
|
|
3601
|
+
mutationId,
|
|
3602
|
+
actor: auth.user.email ?? auth.user.userId
|
|
3603
|
+
});
|
|
3604
|
+
const delivery = await deliverSaved(db, env, ctx, target, saved.delivery);
|
|
3605
|
+
return json({ ok: true, duplicate: saved.duplicate, delivery }, delivery.status === "acknowledged" ? 200 : 202);
|
|
3606
|
+
} catch (error) {
|
|
3607
|
+
if (error instanceof Error && error.message === "mutation_conflict") {
|
|
3608
|
+
return reject4(409, "mutation_conflict", "mutationId was already used for another request");
|
|
3609
|
+
}
|
|
3610
|
+
if (error instanceof Error && /valid authoritative email/.test(error.message)) {
|
|
3611
|
+
return reject4(422, "invalid_email", error.message);
|
|
3612
|
+
}
|
|
3613
|
+
return reject4(409, "revision_conflict", "admission assignments changed; reload and retry");
|
|
3614
|
+
}
|
|
3615
|
+
};
|
|
3616
|
+
async function retryLeaderAdmissionDeliveries(env, ctx, limit = 100) {
|
|
3617
|
+
const db = ctx.makeDb(env);
|
|
3618
|
+
const result = await db.query({ leaderAdmissionDeliveries: { $: { limit } } });
|
|
3619
|
+
const pending = admissionRows(result.leaderAdmissionDeliveries).filter((delivery) => delivery.status !== "acknowledged" && delivery.status !== "rejected").slice(0, Math.max(1, Math.min(limit, 500)));
|
|
3620
|
+
const delivered = [];
|
|
3621
|
+
for (const item of pending) {
|
|
3622
|
+
const target = targetFor(ctx, item.targetId);
|
|
3623
|
+
if (target) delivered.push(await deliverSaved(db, env, ctx, target, item));
|
|
3624
|
+
}
|
|
3625
|
+
return delivered;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3259
3628
|
// src/worker-routes-schedule.ts
|
|
3260
3629
|
import { computeBookableSlots, initCalendar } from "@odla-ai/calendar";
|
|
3261
3630
|
function errCode(err) {
|
|
@@ -4517,16 +4886,16 @@ var crmDeps2 = (db, ctx) => ({
|
|
|
4517
4886
|
});
|
|
4518
4887
|
var handleAdminCrmSync = async (req, url, env, ctx) => {
|
|
4519
4888
|
if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
|
|
4520
|
-
const
|
|
4521
|
-
if (
|
|
4522
|
-
const result = await backfillCrm(crmDeps2(
|
|
4889
|
+
const gate10 = await adminGate(req, env, ctx);
|
|
4890
|
+
if (gate10 instanceof Response) return gate10;
|
|
4891
|
+
const result = await backfillCrm(crmDeps2(gate10.db, ctx));
|
|
4523
4892
|
return json({ ok: true, ...result });
|
|
4524
4893
|
};
|
|
4525
4894
|
var handleAdminPeople = async (req, url, env, ctx) => {
|
|
4526
4895
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
|
|
4527
|
-
const
|
|
4528
|
-
if (
|
|
4529
|
-
const { db } =
|
|
4896
|
+
const gate10 = await adminGate(req, env, ctx);
|
|
4897
|
+
if (gate10 instanceof Response) return gate10;
|
|
4898
|
+
const { db } = gate10;
|
|
4530
4899
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
4531
4900
|
const [data, roleList] = await Promise.all([
|
|
4532
4901
|
db.query({
|
|
@@ -4579,9 +4948,9 @@ var handleAdminPeople = async (req, url, env, ctx) => {
|
|
|
4579
4948
|
};
|
|
4580
4949
|
var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
4581
4950
|
if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
|
|
4582
|
-
const
|
|
4583
|
-
if (
|
|
4584
|
-
const { db } =
|
|
4951
|
+
const gate10 = await adminGate(req, env, ctx);
|
|
4952
|
+
if (gate10 instanceof Response) return gate10;
|
|
4953
|
+
const { db } = gate10;
|
|
4585
4954
|
const targetId = url.searchParams.get("userId") ?? "";
|
|
4586
4955
|
if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
|
|
4587
4956
|
const sk = await getVaultSecret(db, "clerk_secret_key");
|
|
@@ -4592,9 +4961,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
|
|
|
4592
4961
|
};
|
|
4593
4962
|
var handleAdminPeopleRole = async (req, url, env, ctx) => {
|
|
4594
4963
|
if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
|
|
4595
|
-
const
|
|
4596
|
-
if (
|
|
4597
|
-
const { db, actor } =
|
|
4964
|
+
const gate10 = await adminGate(req, env, ctx);
|
|
4965
|
+
if (gate10 instanceof Response) return gate10;
|
|
4966
|
+
const { db, actor } = gate10;
|
|
4598
4967
|
let body;
|
|
4599
4968
|
try {
|
|
4600
4969
|
body = await req.json();
|
|
@@ -4741,7 +5110,7 @@ function subAnnualCents(sub) {
|
|
|
4741
5110
|
}
|
|
4742
5111
|
|
|
4743
5112
|
// src/worker-routes-admin-dashboard.ts
|
|
4744
|
-
async function
|
|
5113
|
+
async function gate3(req, env, ctx) {
|
|
4745
5114
|
const rawDb = ctx.makeDb(env);
|
|
4746
5115
|
const u = await ctx.verifyUser(req, env);
|
|
4747
5116
|
if (!u) return json({ error: "unauthorized" }, 401);
|
|
@@ -4751,7 +5120,7 @@ async function gate2(req, env, ctx) {
|
|
|
4751
5120
|
var rows3 = (v) => Array.isArray(v) ? v : [];
|
|
4752
5121
|
var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
4753
5122
|
if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
|
|
4754
|
-
const got = await
|
|
5123
|
+
const got = await gate3(req, env, ctx);
|
|
4755
5124
|
if (got instanceof Response) return got;
|
|
4756
5125
|
const db = got;
|
|
4757
5126
|
const now = Date.now();
|
|
@@ -4834,7 +5203,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
|
|
|
4834
5203
|
};
|
|
4835
5204
|
var handleAdminBilling = async (req, url, env, ctx) => {
|
|
4836
5205
|
if (req.method !== "GET" || url.pathname !== "/api/admin/billing") return null;
|
|
4837
|
-
const got = await
|
|
5206
|
+
const got = await gate3(req, env, ctx);
|
|
4838
5207
|
if (got instanceof Response) return got;
|
|
4839
5208
|
const db = got;
|
|
4840
5209
|
const sk = await getVaultSecret(db, "stripe_secret_key");
|
|
@@ -4894,7 +5263,7 @@ var handleAdminBilling = async (req, url, env, ctx) => {
|
|
|
4894
5263
|
|
|
4895
5264
|
// src/worker-routes-admin-lifecycle.ts
|
|
4896
5265
|
import { computeBookableSlots as computeBookableSlots2, initCalendar as initCalendar3 } from "@odla-ai/calendar";
|
|
4897
|
-
async function
|
|
5266
|
+
async function gate4(req, env, ctx) {
|
|
4898
5267
|
const rawDb = ctx.makeDb(env);
|
|
4899
5268
|
const u = await ctx.verifyUser(req, env);
|
|
4900
5269
|
if (!u) return json({ error: "unauthorized" }, 401);
|
|
@@ -4920,7 +5289,7 @@ async function loadGroup(db, id) {
|
|
|
4920
5289
|
var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
|
|
4921
5290
|
const m = url.pathname.match(/^\/api\/admin\/meetings\/([0-9a-f-]+)\/reschedule$/);
|
|
4922
5291
|
if (req.method !== "POST" || !m) return null;
|
|
4923
|
-
const got = await
|
|
5292
|
+
const got = await gate4(req, env, ctx);
|
|
4924
5293
|
if (got instanceof Response) return got;
|
|
4925
5294
|
const db = got;
|
|
4926
5295
|
const body = await readJson(req);
|
|
@@ -4961,7 +5330,7 @@ var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
|
|
|
4961
5330
|
var handleAdminMeetingCancel = async (req, url, env, ctx) => {
|
|
4962
5331
|
const m = url.pathname.match(/^\/api\/admin\/meetings\/([0-9a-f-]+)\/cancel$/);
|
|
4963
5332
|
if (req.method !== "POST" || !m) return null;
|
|
4964
|
-
const got = await
|
|
5333
|
+
const got = await gate4(req, env, ctx);
|
|
4965
5334
|
if (got instanceof Response) return got;
|
|
4966
5335
|
const db = got;
|
|
4967
5336
|
const { meetings } = await db.query({ meetings: { $: { where: { id: m[1] }, limit: 1 } } });
|
|
@@ -4984,7 +5353,7 @@ var handleAdminMeetingCancel = async (req, url, env, ctx) => {
|
|
|
4984
5353
|
var handleAdminApprove = async (req, url, env, ctx) => {
|
|
4985
5354
|
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/approve$/);
|
|
4986
5355
|
if (req.method !== "POST" || !m) return null;
|
|
4987
|
-
const got = await
|
|
5356
|
+
const got = await gate4(req, env, ctx);
|
|
4988
5357
|
if (got instanceof Response) return got;
|
|
4989
5358
|
const db = got;
|
|
4990
5359
|
const id = m[1];
|
|
@@ -4997,7 +5366,7 @@ var handleAdminApprove = async (req, url, env, ctx) => {
|
|
|
4997
5366
|
var handleAdminRefund = async (req, url, env, ctx) => {
|
|
4998
5367
|
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/refund$/);
|
|
4999
5368
|
if (req.method !== "POST" || !m) return null;
|
|
5000
|
-
const got = await
|
|
5369
|
+
const got = await gate4(req, env, ctx);
|
|
5001
5370
|
if (got instanceof Response) return got;
|
|
5002
5371
|
const db = got;
|
|
5003
5372
|
const app = await loadApp(db, m[1]);
|
|
@@ -5029,7 +5398,7 @@ var handleAdminRefund = async (req, url, env, ctx) => {
|
|
|
5029
5398
|
var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
5030
5399
|
const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)$/);
|
|
5031
5400
|
if (req.method !== "PATCH" || !m) return null;
|
|
5032
|
-
const got = await
|
|
5401
|
+
const got = await gate4(req, env, ctx);
|
|
5033
5402
|
if (got instanceof Response) return got;
|
|
5034
5403
|
const db = got;
|
|
5035
5404
|
const id = m[1];
|
|
@@ -5057,7 +5426,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
|
|
|
5057
5426
|
};
|
|
5058
5427
|
|
|
5059
5428
|
// src/worker-routes-admin-comms.ts
|
|
5060
|
-
async function
|
|
5429
|
+
async function gate5(req, env, ctx) {
|
|
5061
5430
|
const rawDb = ctx.makeDb(env);
|
|
5062
5431
|
const u = await ctx.verifyUser(req, env);
|
|
5063
5432
|
if (!u) return json({ error: "unauthorized" }, 401);
|
|
@@ -5079,7 +5448,7 @@ var notifyDeps = (db, env) => ({
|
|
|
5079
5448
|
});
|
|
5080
5449
|
var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
5081
5450
|
if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
|
|
5082
|
-
const got = await
|
|
5451
|
+
const got = await gate5(req, env, ctx);
|
|
5083
5452
|
if (got instanceof Response) return got;
|
|
5084
5453
|
const db = got;
|
|
5085
5454
|
const group = await loadGroup2(db, ctx.chapter.id);
|
|
@@ -5141,7 +5510,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
|
|
|
5141
5510
|
};
|
|
5142
5511
|
var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
5143
5512
|
if (req.method !== "GET" || url.pathname !== "/api/admin/email/log") return null;
|
|
5144
|
-
const got = await
|
|
5513
|
+
const got = await gate5(req, env, ctx);
|
|
5145
5514
|
if (got instanceof Response) return got;
|
|
5146
5515
|
const { emailLog } = await got.query({ emailLog: { $: { order: { sentAt: "desc" }, limit: 50 } } });
|
|
5147
5516
|
const sends = (emailLog ?? []).map((r) => ({
|
|
@@ -5158,7 +5527,7 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
|
|
|
5158
5527
|
};
|
|
5159
5528
|
var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
5160
5529
|
if (req.method !== "POST" || url.pathname !== "/api/admin/email/test") return null;
|
|
5161
|
-
const got = await
|
|
5530
|
+
const got = await gate5(req, env, ctx);
|
|
5162
5531
|
if (got instanceof Response) return got;
|
|
5163
5532
|
const db = got;
|
|
5164
5533
|
let body;
|
|
@@ -5186,7 +5555,7 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
|
|
|
5186
5555
|
var handleAdminComms = async (req, url, env, ctx) => {
|
|
5187
5556
|
const m = url.pathname.match(/^\/api\/admin\/people\/([0-9a-fA-F-]+)\/comms$/);
|
|
5188
5557
|
if (req.method !== "GET" || !m) return null;
|
|
5189
|
-
const got = await
|
|
5558
|
+
const got = await gate5(req, env, ctx);
|
|
5190
5559
|
if (got instanceof Response) return got;
|
|
5191
5560
|
const db = got;
|
|
5192
5561
|
const appId = m[1];
|
|
@@ -5224,26 +5593,12 @@ var handleAdminComms = async (req, url, env, ctx) => {
|
|
|
5224
5593
|
};
|
|
5225
5594
|
|
|
5226
5595
|
// src/worker-routes-network.ts
|
|
5227
|
-
import { getRecord, typeSummary } from "@odla-ai/crm";
|
|
5228
|
-
import { signFederatedRequest as
|
|
5229
|
-
|
|
5230
|
-
// src/worker-network-fetch.ts
|
|
5231
|
-
function targetFetcher(env, target) {
|
|
5232
|
-
if (!target.binding) return null;
|
|
5233
|
-
const candidate = env[target.binding];
|
|
5234
|
-
if (typeof candidate !== "object" || candidate === null || !("fetch" in candidate) || typeof candidate.fetch !== "function") {
|
|
5235
|
-
throw new Error(`service binding "${target.binding}" is unavailable`);
|
|
5236
|
-
}
|
|
5237
|
-
return candidate;
|
|
5238
|
-
}
|
|
5239
|
-
function fetchNetworkTarget(env, target, destination, init) {
|
|
5240
|
-
const binding = targetFetcher(env, target);
|
|
5241
|
-
return binding ? binding.fetch(new Request(destination, init)) : globalThis.fetch(destination, init);
|
|
5242
|
-
}
|
|
5596
|
+
import { getRecord as getRecord3, typeSummary } from "@odla-ai/crm";
|
|
5597
|
+
import { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
|
|
5243
5598
|
|
|
5244
5599
|
// src/worker-network-push.ts
|
|
5245
5600
|
import { recordDeliveryAttempt } from "@odla-ai/crm";
|
|
5246
|
-
import { signFederatedRequest } from "@odla-ai/db";
|
|
5601
|
+
import { signFederatedRequest as signFederatedRequest2 } from "@odla-ai/db";
|
|
5247
5602
|
async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
5248
5603
|
const secret = await getVaultSecret(db, target.secretName);
|
|
5249
5604
|
const crmDeps3 = {
|
|
@@ -5277,7 +5632,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
5277
5632
|
const payloadBody = JSON.stringify(payload);
|
|
5278
5633
|
const destination = new URL("/api/network/shared", target.url);
|
|
5279
5634
|
try {
|
|
5280
|
-
const signed = await
|
|
5635
|
+
const signed = await signFederatedRequest2({
|
|
5281
5636
|
secret,
|
|
5282
5637
|
sender: ctx.chapter.id,
|
|
5283
5638
|
method: "POST",
|
|
@@ -5336,7 +5691,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
|
|
|
5336
5691
|
}
|
|
5337
5692
|
|
|
5338
5693
|
// src/worker-routes-network.ts
|
|
5339
|
-
async function
|
|
5694
|
+
async function gate6(req, env, ctx) {
|
|
5340
5695
|
const db = ctx.makeDb(env);
|
|
5341
5696
|
const user = await ctx.verifyUser(req, env);
|
|
5342
5697
|
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
@@ -5345,15 +5700,25 @@ async function gate5(req, env, ctx) {
|
|
|
5345
5700
|
}
|
|
5346
5701
|
var handleAdminNetworkTargets = async (req, url, env, ctx) => {
|
|
5347
5702
|
if (req.method !== "GET" || url.pathname !== "/api/admin/network/targets") return null;
|
|
5348
|
-
const got = await
|
|
5703
|
+
const got = await gate6(req, env, ctx);
|
|
5349
5704
|
if ("response" in got) return got.response;
|
|
5350
5705
|
return json({
|
|
5351
|
-
targets: ctx.chapter.network.targets.map(({
|
|
5706
|
+
targets: ctx.chapter.network.targets.map(({
|
|
5707
|
+
id,
|
|
5708
|
+
name,
|
|
5709
|
+
url: targetUrl,
|
|
5710
|
+
fields,
|
|
5711
|
+
sharedNotes,
|
|
5712
|
+
editableFields,
|
|
5713
|
+
stageTransitions
|
|
5714
|
+
}) => ({
|
|
5352
5715
|
id,
|
|
5353
5716
|
name,
|
|
5354
5717
|
url: targetUrl,
|
|
5355
5718
|
types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
|
|
5356
|
-
sharedNotes
|
|
5719
|
+
sharedNotes,
|
|
5720
|
+
editableFields,
|
|
5721
|
+
stageTransitions
|
|
5357
5722
|
}))
|
|
5358
5723
|
});
|
|
5359
5724
|
};
|
|
@@ -5380,7 +5745,7 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
5380
5745
|
if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
|
|
5381
5746
|
const destination = new URL("/api/network/snapshot", target.url);
|
|
5382
5747
|
try {
|
|
5383
|
-
const headers = await
|
|
5748
|
+
const headers = await signFederatedRequest3({
|
|
5384
5749
|
secret,
|
|
5385
5750
|
sender: ctx.chapter.id,
|
|
5386
5751
|
method: "GET",
|
|
@@ -5421,7 +5786,7 @@ async function snapshotOne(db, env, ctx, target) {
|
|
|
5421
5786
|
}
|
|
5422
5787
|
var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
5423
5788
|
if (req.method !== "GET" || url.pathname !== "/api/admin/network/rollup") return null;
|
|
5424
|
-
const got = await
|
|
5789
|
+
const got = await gate6(req, env, ctx);
|
|
5425
5790
|
if ("response" in got) return got.response;
|
|
5426
5791
|
const targets = await Promise.all(
|
|
5427
5792
|
ctx.chapter.network.targets.map((target) => snapshotOne(got.db, env, ctx, target))
|
|
@@ -5448,7 +5813,7 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
|
|
|
5448
5813
|
};
|
|
5449
5814
|
var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
5450
5815
|
if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
|
|
5451
|
-
const got = await
|
|
5816
|
+
const got = await gate6(req, env, ctx);
|
|
5452
5817
|
if ("response" in got) return got.response;
|
|
5453
5818
|
let body;
|
|
5454
5819
|
try {
|
|
@@ -5463,7 +5828,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
5463
5828
|
if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
|
|
5464
5829
|
const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
|
|
5465
5830
|
if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
|
|
5466
|
-
const record4 = await
|
|
5831
|
+
const record4 = await getRecord3({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
|
|
5467
5832
|
if (!record4) return json({ error: "record not found" }, 404);
|
|
5468
5833
|
const results = await Promise.all(
|
|
5469
5834
|
targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
|
|
@@ -5471,19 +5836,20 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
|
|
|
5471
5836
|
return json({ ok: results.every((result) => result.ok), results });
|
|
5472
5837
|
};
|
|
5473
5838
|
|
|
5474
|
-
// src/worker-routes-network-records.ts
|
|
5475
|
-
import { listRecords } from "@odla-ai/crm";
|
|
5476
|
-
import {
|
|
5839
|
+
// src/worker-routes-network-records-follower.ts
|
|
5840
|
+
import { CRM_NS, CrmInputError, getRecord as getRecord4, listRecords, setStage as setStage2, updateRecord as updateRecord3 } from "@odla-ai/crm";
|
|
5841
|
+
import { verifyFederatedRequest as verifyFederatedRequest6 } from "@odla-ai/db";
|
|
5842
|
+
|
|
5843
|
+
// src/worker-routes-network-records-common.ts
|
|
5477
5844
|
var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
|
|
5478
5845
|
var MAX_PAGE = 50;
|
|
5479
5846
|
var MAX_OFFSET = 1e4;
|
|
5480
5847
|
var MAX_SEARCH = 200;
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
return { db, user };
|
|
5848
|
+
var MAX_BODY = 32768;
|
|
5849
|
+
var RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,191}$/;
|
|
5850
|
+
var MUTATION_ID2 = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{7,127}$/;
|
|
5851
|
+
function bodyTooLarge(body) {
|
|
5852
|
+
return new TextEncoder().encode(body).byteLength > MAX_BODY;
|
|
5487
5853
|
}
|
|
5488
5854
|
function integer(value, fallback, min, max) {
|
|
5489
5855
|
if (value == null || value === "") return fallback;
|
|
@@ -5491,10 +5857,22 @@ function integer(value, fallback, min, max) {
|
|
|
5491
5857
|
const parsed = Number(value);
|
|
5492
5858
|
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
|
|
5493
5859
|
}
|
|
5494
|
-
function projectRecord(record4, allowed2) {
|
|
5860
|
+
function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
|
|
5495
5861
|
const fields = Object.fromEntries(
|
|
5496
5862
|
allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
|
|
5497
5863
|
);
|
|
5864
|
+
const def = crm.type(record4.type);
|
|
5865
|
+
const editFields = editable.map((id) => {
|
|
5866
|
+
const field = def.fields[id];
|
|
5867
|
+
return {
|
|
5868
|
+
id,
|
|
5869
|
+
label: field.label ?? id,
|
|
5870
|
+
type: field.type,
|
|
5871
|
+
required: field.required ?? false,
|
|
5872
|
+
...field.options ? { options: field.options } : {}
|
|
5873
|
+
};
|
|
5874
|
+
});
|
|
5875
|
+
const allowedStages = stagesEnabled ? (def.pipeline?.stages ?? []).flatMap((stage) => crm.canTransition(record4.type, record4, stage.id) ? [] : [{ id: stage.id, label: stage.label ?? stage.id }]) : [];
|
|
5498
5876
|
return {
|
|
5499
5877
|
id: record4.id,
|
|
5500
5878
|
type: record4.type,
|
|
@@ -5502,11 +5880,66 @@ function projectRecord(record4, allowed2) {
|
|
|
5502
5880
|
...record4.stage ? { stage: record4.stage } : {},
|
|
5503
5881
|
fields,
|
|
5504
5882
|
createdAt: record4.createdAt,
|
|
5505
|
-
updatedAt: record4.updatedAt
|
|
5883
|
+
updatedAt: record4.updatedAt,
|
|
5884
|
+
revision: record4.updatedAt,
|
|
5885
|
+
...editFields.length || stagesEnabled ? { edit: { fields: editFields, allowedStages } } : {}
|
|
5506
5886
|
};
|
|
5507
5887
|
}
|
|
5888
|
+
function readerRecord(record4, reader, crm) {
|
|
5889
|
+
return projectRecord(
|
|
5890
|
+
record4,
|
|
5891
|
+
reader.fields[record4.type] ?? [],
|
|
5892
|
+
reader.editableFields[record4.type] ?? [],
|
|
5893
|
+
reader.stageTransitions.includes(record4.type),
|
|
5894
|
+
crm
|
|
5895
|
+
);
|
|
5896
|
+
}
|
|
5897
|
+
function restrictedRecord(record4, target) {
|
|
5898
|
+
if (!record4.edit) return record4;
|
|
5899
|
+
const fields = record4.edit.fields.filter((field) => target.editableFields[record4.type]?.includes(field.id));
|
|
5900
|
+
const allowedStages = target.stageTransitions.includes(record4.type) ? record4.edit.allowedStages : [];
|
|
5901
|
+
const edit = fields.length || target.stageTransitions.includes(record4.type) ? { fields, allowedStages } : void 0;
|
|
5902
|
+
return { ...record4, ...edit ? { edit } : { edit: void 0 } };
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
// src/worker-routes-network-records-follower.ts
|
|
5906
|
+
var NETWORK_MUTATION_META = "chapterNetworkMutation";
|
|
5907
|
+
function mutationFingerprint(input) {
|
|
5908
|
+
const fields = input.fields ? Object.fromEntries(Object.entries(input.fields).sort(([a], [b]) => a.localeCompare(b))) : void 0;
|
|
5909
|
+
return JSON.stringify({
|
|
5910
|
+
recordId: input.recordId,
|
|
5911
|
+
type: input.type,
|
|
5912
|
+
expectedRevision: input.expectedRevision,
|
|
5913
|
+
expectedStage: input.expectedStage,
|
|
5914
|
+
mutationId: input.mutationId,
|
|
5915
|
+
fields,
|
|
5916
|
+
stage: input.stage,
|
|
5917
|
+
actor: input.actor
|
|
5918
|
+
});
|
|
5919
|
+
}
|
|
5920
|
+
function mutationError(error) {
|
|
5921
|
+
if (error?.code === "transact_guard_failed") {
|
|
5922
|
+
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
5923
|
+
}
|
|
5924
|
+
if (error instanceof CrmInputError) {
|
|
5925
|
+
return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
|
|
5926
|
+
}
|
|
5927
|
+
return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
|
|
5928
|
+
}
|
|
5929
|
+
async function priorMutation(db, id) {
|
|
5930
|
+
const result = await db.query({
|
|
5931
|
+
[CRM_NS.activity]: { $: { where: { id }, limit: 1 } }
|
|
5932
|
+
});
|
|
5933
|
+
const row = (result[CRM_NS.activity] ?? [])[0];
|
|
5934
|
+
const meta = row?.meta;
|
|
5935
|
+
const receipt = meta?.[NETWORK_MUTATION_META];
|
|
5936
|
+
return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint } : null;
|
|
5937
|
+
}
|
|
5508
5938
|
var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
5509
|
-
if (req.method !== "GET"
|
|
5939
|
+
if (url.pathname !== "/api/network/records" || req.method !== "GET" && req.method !== "PATCH") return null;
|
|
5940
|
+
if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
|
|
5941
|
+
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
5942
|
+
}
|
|
5510
5943
|
const db = ctx.makeDb(env);
|
|
5511
5944
|
const secret = await getVaultSecret(db, "network_share_secret");
|
|
5512
5945
|
const readers = ctx.chapter.network.readers;
|
|
@@ -5517,8 +5950,105 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5517
5950
|
});
|
|
5518
5951
|
if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
|
|
5519
5952
|
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
5953
|
+
if (!reader) return json({ error: "unauthorized" }, 401);
|
|
5954
|
+
if (req.method === "PATCH") {
|
|
5955
|
+
if (bodyTooLarge(verified.body)) {
|
|
5956
|
+
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
5957
|
+
}
|
|
5958
|
+
let input;
|
|
5959
|
+
try {
|
|
5960
|
+
input = JSON.parse(verified.body);
|
|
5961
|
+
} catch {
|
|
5962
|
+
return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
|
|
5963
|
+
}
|
|
5964
|
+
const fields = input?.fields;
|
|
5965
|
+
const hasFields = Boolean(fields && typeof fields === "object" && !Array.isArray(fields));
|
|
5966
|
+
const hasStage = typeof input?.stage === "string";
|
|
5967
|
+
const actor = input?.actor;
|
|
5968
|
+
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) {
|
|
5969
|
+
return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
|
|
5970
|
+
}
|
|
5971
|
+
const editable = reader.editableFields[input.type];
|
|
5972
|
+
if (hasFields) {
|
|
5973
|
+
const names = Object.keys(fields);
|
|
5974
|
+
if (!editable || names.length === 0 || names.length > 32 || names.some((name) => !editable.includes(name))) {
|
|
5975
|
+
return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
|
|
5976
|
+
}
|
|
5977
|
+
} else if (!reader.stageTransitions.includes(input.type)) {
|
|
5978
|
+
return json({ error: "stage transitions are not delegated for this reader", code: "stage_forbidden" }, 403);
|
|
5979
|
+
}
|
|
5980
|
+
const activityId = `network-edit:${verified.sender}:${input.mutationId}`;
|
|
5981
|
+
const fingerprint = mutationFingerprint(input);
|
|
5982
|
+
const prior = await priorMutation(db, activityId);
|
|
5983
|
+
if (prior) {
|
|
5984
|
+
if (prior.fingerprint !== fingerprint) {
|
|
5985
|
+
return json({ error: "mutationId was already used for different input", code: "mutation_conflict" }, 409);
|
|
5986
|
+
}
|
|
5987
|
+
const replayed = await getRecord4({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
5988
|
+
if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
5989
|
+
const result = {
|
|
5990
|
+
version: 1,
|
|
5991
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
5992
|
+
record: readerRecord(replayed, reader, ctx.chapter.crm),
|
|
5993
|
+
duplicate: true
|
|
5994
|
+
};
|
|
5995
|
+
return json(result);
|
|
5996
|
+
}
|
|
5997
|
+
const record4 = await getRecord4({ crm: ctx.chapter.crm, db }, input.recordId);
|
|
5998
|
+
if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
|
|
5999
|
+
if (record4.updatedAt !== input.expectedRevision) {
|
|
6000
|
+
return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
|
|
6001
|
+
}
|
|
6002
|
+
if (hasStage && (input.expectedStage ?? null) !== (record4.stage ?? null)) {
|
|
6003
|
+
return json({ error: "stage changed since it was reviewed", code: "stale_stage" }, 409);
|
|
6004
|
+
}
|
|
6005
|
+
const actorEmail = typeof actor.email === "string" && actor.email.length <= 320 ? actor.email : void 0;
|
|
6006
|
+
const auditMeta = {
|
|
6007
|
+
[NETWORK_MUTATION_META]: {
|
|
6008
|
+
version: 1,
|
|
6009
|
+
sourceId: verified.sender,
|
|
6010
|
+
actorId: actor.id,
|
|
6011
|
+
...actorEmail ? { actorEmail } : {},
|
|
6012
|
+
mutationId: input.mutationId,
|
|
6013
|
+
fingerprint
|
|
6014
|
+
}
|
|
6015
|
+
};
|
|
6016
|
+
try {
|
|
6017
|
+
const deps = { crm: ctx.chapter.crm, db };
|
|
6018
|
+
const updated = hasFields ? await updateRecord3(deps, {
|
|
6019
|
+
id: record4.id,
|
|
6020
|
+
input: fields,
|
|
6021
|
+
mutationId: activityId,
|
|
6022
|
+
expectedUpdatedAt: input.expectedRevision,
|
|
6023
|
+
audit: {
|
|
6024
|
+
id: activityId,
|
|
6025
|
+
body: `Profile edited remotely by ${verified.sender}`,
|
|
6026
|
+
authorId: `network:${verified.sender}`,
|
|
6027
|
+
meta: auditMeta
|
|
6028
|
+
}
|
|
6029
|
+
}) : await setStage2(deps, {
|
|
6030
|
+
id: record4.id,
|
|
6031
|
+
to: input.stage,
|
|
6032
|
+
authorId: `network:${verified.sender}`,
|
|
6033
|
+
mutationId: activityId,
|
|
6034
|
+
expectedUpdatedAt: input.expectedRevision,
|
|
6035
|
+
expectedStage: input.expectedStage ?? null,
|
|
6036
|
+
activityId,
|
|
6037
|
+
activityMeta: auditMeta
|
|
6038
|
+
});
|
|
6039
|
+
const result = {
|
|
6040
|
+
version: 1,
|
|
6041
|
+
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
6042
|
+
record: readerRecord(updated.record, reader, ctx.chapter.crm),
|
|
6043
|
+
duplicate: updated.duplicate
|
|
6044
|
+
};
|
|
6045
|
+
return json(result);
|
|
6046
|
+
} catch (error) {
|
|
6047
|
+
return mutationError(error);
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
5520
6050
|
const type = url.searchParams.get("type") ?? "";
|
|
5521
|
-
const allowed2 = reader
|
|
6051
|
+
const allowed2 = reader.fields[type];
|
|
5522
6052
|
if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
|
|
5523
6053
|
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
5524
6054
|
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
@@ -5540,14 +6070,24 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5540
6070
|
version: 1,
|
|
5541
6071
|
site: { id: ctx.chapter.id, name: ctx.chapter.name },
|
|
5542
6072
|
type,
|
|
5543
|
-
records: page.records.map((record4) =>
|
|
6073
|
+
records: page.records.map((record4) => readerRecord(record4, reader, ctx.chapter.crm)),
|
|
5544
6074
|
total: page.total,
|
|
5545
6075
|
limit: page.limit,
|
|
5546
6076
|
offset: page.offset
|
|
5547
6077
|
};
|
|
5548
6078
|
return json(response);
|
|
5549
6079
|
};
|
|
5550
|
-
|
|
6080
|
+
|
|
6081
|
+
// src/worker-routes-network-records-admin.ts
|
|
6082
|
+
import { signFederatedRequest as signFederatedRequest4 } from "@odla-ai/db";
|
|
6083
|
+
async function gate7(req, env, ctx) {
|
|
6084
|
+
const db = ctx.makeDb(env);
|
|
6085
|
+
const user = await ctx.verifyUser(req, env);
|
|
6086
|
+
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
6087
|
+
if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
|
|
6088
|
+
return { db, user };
|
|
6089
|
+
}
|
|
6090
|
+
function targetFor2(ctx, id) {
|
|
5551
6091
|
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
5552
6092
|
}
|
|
5553
6093
|
async function readTargetRecords(db, env, ctx, target, query) {
|
|
@@ -5556,12 +6096,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
5556
6096
|
const destination = new URL("/api/network/records", target.url);
|
|
5557
6097
|
destination.search = query.toString();
|
|
5558
6098
|
try {
|
|
5559
|
-
const headers = await
|
|
5560
|
-
secret,
|
|
5561
|
-
sender: ctx.chapter.id,
|
|
5562
|
-
method: "GET",
|
|
5563
|
-
url: destination
|
|
5564
|
-
});
|
|
6099
|
+
const headers = await signFederatedRequest4({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
|
|
5565
6100
|
const response = await fetchNetworkTarget(env, target, destination, {
|
|
5566
6101
|
headers,
|
|
5567
6102
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -5574,18 +6109,93 @@ async function readTargetRecords(db, env, ctx, target, query) {
|
|
|
5574
6109
|
error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`)
|
|
5575
6110
|
};
|
|
5576
6111
|
}
|
|
5577
|
-
return {
|
|
6112
|
+
return {
|
|
6113
|
+
ok: true,
|
|
6114
|
+
page: { ...body, records: body.records.map((record4) => restrictedRecord(record4, target)) }
|
|
6115
|
+
};
|
|
5578
6116
|
} catch (error) {
|
|
5579
6117
|
return { ok: false, error: error instanceof Error ? error.message : "record read failed" };
|
|
5580
6118
|
}
|
|
5581
6119
|
}
|
|
6120
|
+
async function mutateTargetRecord(db, env, ctx, target, input) {
|
|
6121
|
+
const fields = input.fields;
|
|
6122
|
+
if (fields) {
|
|
6123
|
+
const allowed2 = target.editableFields[input.type];
|
|
6124
|
+
if (!allowed2 || Object.keys(fields).some((name) => !allowed2.includes(name))) {
|
|
6125
|
+
return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
|
|
6126
|
+
}
|
|
6127
|
+
} else if (!target.stageTransitions.includes(input.type)) {
|
|
6128
|
+
return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
|
|
6129
|
+
}
|
|
6130
|
+
const secret = await getVaultSecret(db, target.secretName);
|
|
6131
|
+
if (!secret) return json({ error: "edge secret is missing", code: "edge_unavailable" }, 502);
|
|
6132
|
+
const destination = new URL("/api/network/records", target.url);
|
|
6133
|
+
const body = JSON.stringify(input);
|
|
6134
|
+
try {
|
|
6135
|
+
const headers = await signFederatedRequest4({
|
|
6136
|
+
secret,
|
|
6137
|
+
sender: ctx.chapter.id,
|
|
6138
|
+
method: "PATCH",
|
|
6139
|
+
url: destination,
|
|
6140
|
+
body
|
|
6141
|
+
});
|
|
6142
|
+
const response = await fetchNetworkTarget(env, target, destination, {
|
|
6143
|
+
method: "PATCH",
|
|
6144
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
6145
|
+
body,
|
|
6146
|
+
signal: AbortSignal.timeout(1e4)
|
|
6147
|
+
});
|
|
6148
|
+
const payload = await response.json().catch(() => null);
|
|
6149
|
+
const site = payload && "site" in payload ? payload.site : void 0;
|
|
6150
|
+
if (!response.ok || !payload || !("record" in payload) || site && site.id !== target.id) {
|
|
6151
|
+
const status = [401, 403, 404, 409, 422].includes(response.status) ? response.status : 502;
|
|
6152
|
+
return json({
|
|
6153
|
+
error: payload && "error" in payload && typeof payload.error === "string" ? payload.error : `follower returned ${response.status}`,
|
|
6154
|
+
...payload && "code" in payload && typeof payload.code === "string" ? { code: payload.code } : {}
|
|
6155
|
+
}, status);
|
|
6156
|
+
}
|
|
6157
|
+
return json({ ...payload, record: restrictedRecord(payload.record, target) });
|
|
6158
|
+
} catch {
|
|
6159
|
+
return json({ error: "follower mutation request failed", code: "follower_unavailable" }, 502);
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
5582
6162
|
var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
5583
|
-
if (req.method !== "GET"
|
|
5584
|
-
|
|
6163
|
+
if (url.pathname !== "/api/admin/network/records" || req.method !== "GET" && req.method !== "PATCH") return null;
|
|
6164
|
+
if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
|
|
6165
|
+
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
6166
|
+
}
|
|
6167
|
+
const got = await gate7(req, env, ctx);
|
|
5585
6168
|
if ("response" in got) return got.response;
|
|
6169
|
+
if (req.method === "PATCH") {
|
|
6170
|
+
let input;
|
|
6171
|
+
try {
|
|
6172
|
+
const body = await req.text();
|
|
6173
|
+
if (bodyTooLarge(body)) {
|
|
6174
|
+
return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
|
|
6175
|
+
}
|
|
6176
|
+
input = JSON.parse(body);
|
|
6177
|
+
} catch {
|
|
6178
|
+
return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
|
|
6179
|
+
}
|
|
6180
|
+
const target2 = targetFor2(ctx, input?.targetId);
|
|
6181
|
+
const hasFields = Boolean(input?.fields && typeof input.fields === "object" && !Array.isArray(input.fields));
|
|
6182
|
+
const hasStage = typeof input?.stage === "string";
|
|
6183
|
+
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) {
|
|
6184
|
+
return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
|
|
6185
|
+
}
|
|
6186
|
+
return mutateTargetRecord(got.db, env, ctx, target2, {
|
|
6187
|
+
type: input.type,
|
|
6188
|
+
recordId: input.recordId,
|
|
6189
|
+
expectedRevision: input.expectedRevision,
|
|
6190
|
+
mutationId: input.mutationId,
|
|
6191
|
+
...input.expectedStage !== void 0 ? { expectedStage: input.expectedStage } : {},
|
|
6192
|
+
...hasFields ? { fields: input.fields } : { stage: input.stage },
|
|
6193
|
+
actor: { id: got.user.userId, ...got.user.email ? { email: got.user.email } : {} }
|
|
6194
|
+
});
|
|
6195
|
+
}
|
|
5586
6196
|
const targetId = url.searchParams.get("targetId") ?? "";
|
|
5587
6197
|
const type = url.searchParams.get("type") ?? "";
|
|
5588
|
-
const target =
|
|
6198
|
+
const target = targetFor2(ctx, targetId);
|
|
5589
6199
|
if (!target || !IDENTIFIER.test(type)) return json({ error: "configured targetId and record type are required" }, 400);
|
|
5590
6200
|
const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
|
|
5591
6201
|
const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
|
|
@@ -5595,21 +6205,15 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
|
|
|
5595
6205
|
}
|
|
5596
6206
|
const query = new URLSearchParams({ type, limit: String(limit), offset: String(offset) });
|
|
5597
6207
|
if (search) query.set("search", search);
|
|
5598
|
-
const result = await readTargetRecords(
|
|
5599
|
-
got.db,
|
|
5600
|
-
env,
|
|
5601
|
-
ctx,
|
|
5602
|
-
target,
|
|
5603
|
-
query
|
|
5604
|
-
);
|
|
6208
|
+
const result = await readTargetRecords(got.db, env, ctx, target, query);
|
|
5605
6209
|
return result.ok ? json(result.page) : json({ error: result.error }, 502);
|
|
5606
6210
|
};
|
|
5607
6211
|
|
|
5608
6212
|
// src/worker-routes-network-notes.ts
|
|
5609
6213
|
var IDENTIFIER2 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
5610
|
-
var
|
|
5611
|
-
var
|
|
5612
|
-
async function
|
|
6214
|
+
var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
6215
|
+
var MAX_NOTE3 = 4e3;
|
|
6216
|
+
async function gate8(req, env, ctx) {
|
|
5613
6217
|
const db = ctx.makeDb(env);
|
|
5614
6218
|
const user = await ctx.verifyUser(req, env);
|
|
5615
6219
|
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
@@ -5621,7 +6225,7 @@ function noteIdentity(url, ctx) {
|
|
|
5621
6225
|
const recordType = url.searchParams.get("type") ?? "";
|
|
5622
6226
|
const recordId = url.searchParams.get("recordId") ?? "";
|
|
5623
6227
|
const configured = ctx.chapter.network.targets.some((target) => target.id === targetId);
|
|
5624
|
-
if (!configured || !IDENTIFIER2.test(recordType) || !
|
|
6228
|
+
if (!configured || !IDENTIFIER2.test(recordType) || !RECORD_ID2.test(recordId)) return null;
|
|
5625
6229
|
return {
|
|
5626
6230
|
targetId,
|
|
5627
6231
|
recordType,
|
|
@@ -5643,7 +6247,7 @@ function asNote(row) {
|
|
|
5643
6247
|
}
|
|
5644
6248
|
var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
5645
6249
|
if (url.pathname !== "/api/admin/network/notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
5646
|
-
const got = await
|
|
6250
|
+
const got = await gate8(req, env, ctx);
|
|
5647
6251
|
if ("response" in got) return got.response;
|
|
5648
6252
|
const identity = noteIdentity(url, ctx);
|
|
5649
6253
|
if (!identity) return json({ error: "valid targetId, type, and recordId are required" }, 400);
|
|
@@ -5660,7 +6264,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
5660
6264
|
return json({ notes: (result.networkNotes ?? []).map((row) => asNote(row)) });
|
|
5661
6265
|
}
|
|
5662
6266
|
const contentLength = Number(req.headers.get("content-length") ?? 0);
|
|
5663
|
-
if (contentLength >
|
|
6267
|
+
if (contentLength > MAX_NOTE3 * 2) return json({ error: "note is too large" }, 413);
|
|
5664
6268
|
let input;
|
|
5665
6269
|
try {
|
|
5666
6270
|
input = await req.json();
|
|
@@ -5668,7 +6272,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
5668
6272
|
return json({ error: "invalid JSON body" }, 400);
|
|
5669
6273
|
}
|
|
5670
6274
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
5671
|
-
if (!body || body.length >
|
|
6275
|
+
if (!body || body.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
|
|
5672
6276
|
const id = crypto.randomUUID();
|
|
5673
6277
|
const note = {
|
|
5674
6278
|
id,
|
|
@@ -5692,16 +6296,16 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
|
|
|
5692
6296
|
// src/worker-routes-network-shared-notes.ts
|
|
5693
6297
|
import {
|
|
5694
6298
|
addActivity,
|
|
5695
|
-
CRM_NS,
|
|
5696
|
-
getRecord as
|
|
6299
|
+
CRM_NS as CRM_NS2,
|
|
6300
|
+
getRecord as getRecord5
|
|
5697
6301
|
} from "@odla-ai/crm";
|
|
5698
|
-
import { signFederatedRequest as
|
|
6302
|
+
import { signFederatedRequest as signFederatedRequest5, verifyFederatedRequest as verifyFederatedRequest7 } from "@odla-ai/db";
|
|
5699
6303
|
var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
|
|
5700
|
-
var
|
|
5701
|
-
var
|
|
5702
|
-
var
|
|
6304
|
+
var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
6305
|
+
var MUTATION_ID3 = /^[A-Za-z0-9_-]{16,128}$/;
|
|
6306
|
+
var MAX_NOTE4 = 4e3;
|
|
5703
6307
|
var NETWORK_META = "chapterNetwork";
|
|
5704
|
-
async function
|
|
6308
|
+
async function gate9(req, env, ctx) {
|
|
5705
6309
|
const db = ctx.makeDb(env);
|
|
5706
6310
|
const user = await ctx.verifyUser(req, env);
|
|
5707
6311
|
if (!user) return { response: json({ error: "unauthorized" }, 401) };
|
|
@@ -5724,7 +6328,7 @@ function sharedNote(activity, sourceId) {
|
|
|
5724
6328
|
}
|
|
5725
6329
|
async function listSharedNotes(db, sourceId, recordId) {
|
|
5726
6330
|
const result = await db.query({
|
|
5727
|
-
[
|
|
6331
|
+
[CRM_NS2.activity]: {
|
|
5728
6332
|
$: {
|
|
5729
6333
|
where: {
|
|
5730
6334
|
and: [
|
|
@@ -5737,7 +6341,7 @@ async function listSharedNotes(db, sourceId, recordId) {
|
|
|
5737
6341
|
}
|
|
5738
6342
|
}
|
|
5739
6343
|
});
|
|
5740
|
-
return (result[
|
|
6344
|
+
return (result[CRM_NS2.activity] ?? []).flatMap((activity) => {
|
|
5741
6345
|
const note = sharedNote(activity, sourceId);
|
|
5742
6346
|
return note ? [note] : [];
|
|
5743
6347
|
});
|
|
@@ -5747,7 +6351,7 @@ function sourceLabel(sourceId) {
|
|
|
5747
6351
|
}
|
|
5748
6352
|
var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
5749
6353
|
if (url.pathname !== "/api/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
5750
|
-
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) >
|
|
6354
|
+
if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_NOTE4 * 2) {
|
|
5751
6355
|
return json({ error: "note is too large" }, 413);
|
|
5752
6356
|
}
|
|
5753
6357
|
const db = ctx.makeDb(env);
|
|
@@ -5762,10 +6366,10 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5762
6366
|
const reader = readers.find((candidate) => candidate.id === verified.sender);
|
|
5763
6367
|
const recordType = url.searchParams.get("type") ?? "";
|
|
5764
6368
|
const recordId = url.searchParams.get("recordId") ?? "";
|
|
5765
|
-
if (!IDENTIFIER3.test(recordType) || !
|
|
6369
|
+
if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
|
|
5766
6370
|
return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
|
|
5767
6371
|
}
|
|
5768
|
-
const record4 = await
|
|
6372
|
+
const record4 = await getRecord5({ crm: ctx.chapter.crm, db }, recordId);
|
|
5769
6373
|
if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
|
|
5770
6374
|
if (req.method === "GET") {
|
|
5771
6375
|
return json({
|
|
@@ -5782,8 +6386,8 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5782
6386
|
}
|
|
5783
6387
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
5784
6388
|
const mutationId = typeof input.mutationId === "string" ? input.mutationId : "";
|
|
5785
|
-
if (!body || body.length >
|
|
5786
|
-
return json({ error: `body must be 1\u2013${
|
|
6389
|
+
if (!body || body.length > MAX_NOTE4 || !MUTATION_ID3.test(mutationId)) {
|
|
6390
|
+
return json({ error: `body must be 1\u2013${MAX_NOTE4} characters and mutationId must be valid` }, 400);
|
|
5787
6391
|
}
|
|
5788
6392
|
const createdAt = Date.now();
|
|
5789
6393
|
const id = `network:${verified.sender}:${mutationId}`;
|
|
@@ -5820,18 +6424,18 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5820
6424
|
};
|
|
5821
6425
|
return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
|
|
5822
6426
|
};
|
|
5823
|
-
function
|
|
6427
|
+
function targetFor3(ctx, id) {
|
|
5824
6428
|
return ctx.chapter.network.targets.find((target) => target.id === id);
|
|
5825
6429
|
}
|
|
5826
6430
|
var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
5827
6431
|
if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
|
|
5828
|
-
const got = await
|
|
6432
|
+
const got = await gate9(req, env, ctx);
|
|
5829
6433
|
if ("response" in got) return got.response;
|
|
5830
6434
|
const targetId = url.searchParams.get("targetId") ?? "";
|
|
5831
6435
|
const recordType = url.searchParams.get("type") ?? "";
|
|
5832
6436
|
const recordId = url.searchParams.get("recordId") ?? "";
|
|
5833
|
-
const target =
|
|
5834
|
-
if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !
|
|
6437
|
+
const target = targetFor3(ctx, targetId);
|
|
6438
|
+
if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId)) {
|
|
5835
6439
|
return json({ error: "configured targetId, type, and recordId are required" }, 400);
|
|
5836
6440
|
}
|
|
5837
6441
|
let body = "";
|
|
@@ -5843,7 +6447,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5843
6447
|
return json({ error: "invalid JSON body" }, 400);
|
|
5844
6448
|
}
|
|
5845
6449
|
const note = typeof input.body === "string" ? input.body.trim() : "";
|
|
5846
|
-
if (!note || note.length >
|
|
6450
|
+
if (!note || note.length > MAX_NOTE4) return json({ error: `note must be 1\u2013${MAX_NOTE4} characters` }, 400);
|
|
5847
6451
|
body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
|
|
5848
6452
|
}
|
|
5849
6453
|
const secret = await getVaultSecret(got.db, target.secretName);
|
|
@@ -5851,7 +6455,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
|
|
|
5851
6455
|
const destination = new URL("/api/network/shared-notes", target.url);
|
|
5852
6456
|
destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
|
|
5853
6457
|
try {
|
|
5854
|
-
const headers = await
|
|
6458
|
+
const headers = await signFederatedRequest5({
|
|
5855
6459
|
secret,
|
|
5856
6460
|
sender: ctx.chapter.id,
|
|
5857
6461
|
method: req.method,
|
|
@@ -6443,11 +7047,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
|
|
|
6443
7047
|
};
|
|
6444
7048
|
|
|
6445
7049
|
// src/signup-control-delivery.ts
|
|
6446
|
-
import { signFederatedRequest as
|
|
7050
|
+
import { signFederatedRequest as signFederatedRequest6 } from "@odla-ai/db";
|
|
6447
7051
|
async function deliverSignupControl(options) {
|
|
6448
7052
|
const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
|
|
6449
7053
|
const body = JSON.stringify(options.envelope);
|
|
6450
|
-
const signed = await
|
|
7054
|
+
const signed = await signFederatedRequest6({
|
|
6451
7055
|
secret: options.secret,
|
|
6452
7056
|
sender: options.sender,
|
|
6453
7057
|
method: "POST",
|
|
@@ -6468,11 +7072,11 @@ async function deliverSignupControl(options) {
|
|
|
6468
7072
|
}
|
|
6469
7073
|
|
|
6470
7074
|
// src/member-content-delivery.ts
|
|
6471
|
-
import { signFederatedRequest as
|
|
7075
|
+
import { signFederatedRequest as signFederatedRequest7 } from "@odla-ai/db";
|
|
6472
7076
|
async function deliverMemberContent(options) {
|
|
6473
7077
|
const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
|
|
6474
7078
|
const body = JSON.stringify(options.envelope);
|
|
6475
|
-
const signed = await
|
|
7079
|
+
const signed = await signFederatedRequest7({
|
|
6476
7080
|
secret: options.secret,
|
|
6477
7081
|
sender: options.sender,
|
|
6478
7082
|
method: "POST",
|
|
@@ -6493,11 +7097,11 @@ async function deliverMemberContent(options) {
|
|
|
6493
7097
|
}
|
|
6494
7098
|
|
|
6495
7099
|
// src/builder-library-delivery.ts
|
|
6496
|
-
import { signFederatedRequest as
|
|
7100
|
+
import { signFederatedRequest as signFederatedRequest8 } from "@odla-ai/db";
|
|
6497
7101
|
async function deliverBuilderLibrary(options) {
|
|
6498
7102
|
const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
|
|
6499
7103
|
const body = JSON.stringify(options.envelope);
|
|
6500
|
-
const signed = await
|
|
7104
|
+
const signed = await signFederatedRequest8({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
|
|
6501
7105
|
const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
|
|
6502
7106
|
const result = await response.json();
|
|
6503
7107
|
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
@@ -6507,31 +7111,6 @@ async function deliverBuilderLibrary(options) {
|
|
|
6507
7111
|
return result;
|
|
6508
7112
|
}
|
|
6509
7113
|
|
|
6510
|
-
// src/admission-grants-delivery.ts
|
|
6511
|
-
import { signFederatedRequest as signFederatedRequest8 } from "@odla-ai/db";
|
|
6512
|
-
async function deliverAdmissionGrants(options) {
|
|
6513
|
-
const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
|
|
6514
|
-
const body = JSON.stringify(options.envelope);
|
|
6515
|
-
const signed = await signFederatedRequest8({
|
|
6516
|
-
secret: options.secret,
|
|
6517
|
-
sender: options.sender,
|
|
6518
|
-
method: "POST",
|
|
6519
|
-
url: destination,
|
|
6520
|
-
body
|
|
6521
|
-
});
|
|
6522
|
-
const response = await (options.fetch ?? globalThis.fetch)(destination, {
|
|
6523
|
-
method: "POST",
|
|
6524
|
-
headers: { ...signed, "content-type": "application/json" },
|
|
6525
|
-
body
|
|
6526
|
-
});
|
|
6527
|
-
const result = await response.json();
|
|
6528
|
-
if (!response.ok || !("ok" in result) || result.ok !== true) {
|
|
6529
|
-
const failure = result;
|
|
6530
|
-
throw new Error(`admission grants rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
|
|
6531
|
-
}
|
|
6532
|
-
return result;
|
|
6533
|
-
}
|
|
6534
|
-
|
|
6535
7114
|
// src/signup-control-theme.ts
|
|
6536
7115
|
var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
|
|
6537
7116
|
var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
|
|
@@ -6691,6 +7270,7 @@ var BUILTIN_ROUTES = [
|
|
|
6691
7270
|
handleAdminPeopleRole,
|
|
6692
7271
|
handleAdminCrmSync,
|
|
6693
7272
|
handleAdminAdmissionGrants,
|
|
7273
|
+
handleAdminNetworkAdmissions,
|
|
6694
7274
|
handleAdminClerkPrivateProfileSync,
|
|
6695
7275
|
// Aggregation
|
|
6696
7276
|
handleAdminDashboard,
|
|
@@ -6735,6 +7315,7 @@ function chapterWorker(options) {
|
|
|
6735
7315
|
};
|
|
6736
7316
|
}
|
|
6737
7317
|
export {
|
|
7318
|
+
AdmissionGrantDeliveryError,
|
|
6738
7319
|
chapterWorker,
|
|
6739
7320
|
createWorkerContext,
|
|
6740
7321
|
deliverAdmissionGrants,
|
|
@@ -6742,6 +7323,7 @@ export {
|
|
|
6742
7323
|
deliverMemberContent,
|
|
6743
7324
|
deliverSignupControl,
|
|
6744
7325
|
renderActiveSignupControlHtml,
|
|
7326
|
+
retryLeaderAdmissionDeliveries,
|
|
6745
7327
|
withActiveSignupControlTheme
|
|
6746
7328
|
};
|
|
6747
7329
|
//# sourceMappingURL=index.js.map
|