@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.
@@ -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 authority = ctx.chapter.signupControl;
3142
- if (!authority) return json({ error: "not found" }, 404);
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(db, authority.secretName);
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: [authority.sourceId] });
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 !== authority.sourceId) return reject3("wrong_source", 403);
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, authority.sourceId, ctx.chapter.id, env.ODLA_ENV);
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,365 @@ 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
+ function targetFetcher(env, target) {
3342
+ if (!target.binding) return null;
3343
+ const candidate = env[target.binding];
3344
+ if (typeof candidate !== "object" || candidate === null || !("fetch" in candidate) || typeof candidate.fetch !== "function") {
3345
+ throw new Error(`service binding "${target.binding}" is unavailable`);
3346
+ }
3347
+ return candidate;
3348
+ }
3349
+ function fetchNetworkTarget(env, target, destination, init) {
3350
+ const binding = targetFetcher(env, target);
3351
+ return binding ? binding.fetch(new Request(destination, init)) : globalThis.fetch(destination, init);
3352
+ }
3353
+
3354
+ // src/worker-network-admission-state.ts
3355
+ var import_crm5 = require("@odla-ai/crm");
3356
+ var MAX_ASSIGNMENTS = 2e3;
3357
+ var admissionRows = (value) => Array.isArray(value) ? value : [];
3358
+ function assignmentId(targetId, personId) {
3359
+ return `leader-admission:${targetId}:${personId}`;
3360
+ }
3361
+ function deliveryId(targetId, environment) {
3362
+ return `leader-admission-delivery:${targetId}:${environment}`;
3363
+ }
3364
+ async function loadLeaderAdmissionAssignments(db, targetId) {
3365
+ const result = await db.query({ leaderAdmissionAssignments: { $: {
3366
+ where: { targetId },
3367
+ order: { updatedAt: "desc" },
3368
+ limit: MAX_ASSIGNMENTS
3369
+ } } });
3370
+ return admissionRows(result.leaderAdmissionAssignments);
3371
+ }
3372
+ async function loadLeaderAdmissionDelivery(db, targetId, environment) {
3373
+ const id = deliveryId(targetId, environment);
3374
+ const result = await db.query({ leaderAdmissionDeliveries: { $: { where: { id }, limit: 1 } } });
3375
+ return admissionRows(result.leaderAdmissionDeliveries)[0] ?? null;
3376
+ }
3377
+ function personAdmissionEmail(ctx, record4) {
3378
+ if (record4.type !== "person") return null;
3379
+ const field = ctx.chapter.crm.config.types.person?.emailField;
3380
+ if (!field) return null;
3381
+ const raw = record4.fields[field] ?? record4.primaryEmail;
3382
+ if (typeof raw !== "string") return null;
3383
+ const normalized = normalizeAdmissionEmail(raw);
3384
+ return isValidEmail(normalized) ? normalized : null;
3385
+ }
3386
+ async function grantInput(db, ctx, assignment) {
3387
+ const person = await (0, import_crm5.getRecord)({ crm: ctx.chapter.crm, db }, assignment.personId);
3388
+ const email = person ? personAdmissionEmail(ctx, person) : null;
3389
+ if (!email) throw new Error(`person ${assignment.personId} has no valid authoritative email`);
3390
+ return {
3391
+ id: assignment.grantId,
3392
+ email,
3393
+ decision: assignment.decision,
3394
+ ...assignment.note ? { note: assignment.note } : {},
3395
+ ...assignment.expiresAt !== void 0 ? { expiresAt: assignment.expiresAt } : {}
3396
+ };
3397
+ }
3398
+ async function persistLeaderAdmissionDesiredSet(opts) {
3399
+ const now = Date.now();
3400
+ const current = await loadLeaderAdmissionAssignments(opts.db, opts.target.id);
3401
+ const id = assignmentId(opts.target.id, opts.personId);
3402
+ const existing = current.find((row) => row.id === id);
3403
+ const digest = await signupControlDigest({
3404
+ targetId: opts.target.id,
3405
+ personId: opts.personId,
3406
+ decision: opts.decision,
3407
+ note: opts.note ?? null,
3408
+ expiresAt: opts.expiresAt ?? null,
3409
+ active: opts.active
3410
+ });
3411
+ const mutationReceiptId = `leader-admission-mutation:${opts.target.id}:${opts.mutationId}`;
3412
+ const priorMutation2 = await opts.db.query({ leaderAdmissionMutations: { $: {
3413
+ where: { id: mutationReceiptId },
3414
+ limit: 1
3415
+ } } });
3416
+ const receipt = admissionRows(priorMutation2.leaderAdmissionMutations)[0];
3417
+ if (receipt) {
3418
+ if (receipt.requestDigest !== digest || receipt.personId !== opts.personId) {
3419
+ throw new Error("mutation_conflict");
3420
+ }
3421
+ const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
3422
+ if (!saved) throw new Error("delivery_missing");
3423
+ return { delivery: saved, duplicate: true };
3424
+ }
3425
+ if (existing?.lastMutationId === opts.mutationId) {
3426
+ if (existing.lastRequestDigest !== digest) throw new Error("mutation_conflict");
3427
+ const saved = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
3428
+ if (!saved) throw new Error("delivery_missing");
3429
+ return { delivery: saved, duplicate: true };
3430
+ }
3431
+ const assignment = {
3432
+ id,
3433
+ targetId: opts.target.id,
3434
+ personId: opts.personId,
3435
+ grantId: existing?.grantId ?? crypto.randomUUID().replaceAll("-", ""),
3436
+ decision: opts.decision,
3437
+ ...opts.note ? { note: opts.note } : {},
3438
+ ...opts.expiresAt !== void 0 ? { expiresAt: opts.expiresAt } : {},
3439
+ active: opts.active,
3440
+ lastMutationId: opts.mutationId,
3441
+ lastRequestDigest: digest,
3442
+ createdAt: existing?.createdAt ?? now,
3443
+ createdBy: existing?.createdBy ?? opts.actor,
3444
+ updatedAt: now,
3445
+ updatedBy: opts.actor
3446
+ };
3447
+ const desired = current.filter((row) => row.id !== id && row.active).concat(assignment.active ? [assignment] : []);
3448
+ const grants = await Promise.all(desired.map((row) => grantInput(opts.db, opts.ctx, row)));
3449
+ const previous = await loadLeaderAdmissionDelivery(opts.db, opts.target.id, opts.env.ODLA_ENV);
3450
+ const revision = (previous?.revision ?? 0) + 1;
3451
+ const envelope = await createAdmissionGrantEnvelope({
3452
+ schemaVersion: 1,
3453
+ revision,
3454
+ generatedAt: now,
3455
+ source: { appId: opts.ctx.chapter.id, actor: opts.actor },
3456
+ target: { chapterId: opts.target.id, environment: opts.env.ODLA_ENV },
3457
+ grants
3458
+ });
3459
+ const delivery = {
3460
+ id: deliveryId(opts.target.id, opts.env.ODLA_ENV),
3461
+ targetId: opts.target.id,
3462
+ environment: opts.env.ODLA_ENV,
3463
+ revision,
3464
+ digest: envelope.digest,
3465
+ envelope,
3466
+ status: "saved",
3467
+ attempts: 0,
3468
+ updatedAt: now
3469
+ };
3470
+ const ops = [
3471
+ { t: "update", ns: "leaderAdmissionAssignments", id, attrs: assignment },
3472
+ { t: "update", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: delivery },
3473
+ { t: "update", ns: "leaderAdmissionMutations", id: mutationReceiptId, attrs: {
3474
+ id: mutationReceiptId,
3475
+ targetId: opts.target.id,
3476
+ personId: opts.personId,
3477
+ requestDigest: digest,
3478
+ revision,
3479
+ createdAt: now
3480
+ } }
3481
+ ];
3482
+ const retract = [
3483
+ ...!opts.note && existing?.note !== void 0 ? ["note"] : [],
3484
+ ...opts.expiresAt === void 0 && existing?.expiresAt !== void 0 ? ["expiresAt"] : []
3485
+ ];
3486
+ if (retract.length) ops.push({ t: "retract", ns: "leaderAdmissionAssignments", id, attrs: retract });
3487
+ const tx = await opts.db.transact(ops, {
3488
+ mutationId: `leader-admission:${opts.target.id}:${opts.mutationId}`,
3489
+ guards: previous ? [{ ns: "leaderAdmissionDeliveries", id: previous.id, exists: true, equals: { revision: previous.revision } }] : [{ ns: "leaderAdmissionDeliveries", id: delivery.id, exists: false }]
3490
+ });
3491
+ return { delivery, duplicate: tx.duplicate };
3492
+ }
3493
+
3494
+ // src/worker-routes-network-admission.ts
3495
+ var PATH = "/api/admin/network/admissions";
3496
+ var RETRY_PATH = "/api/admin/network/admissions/retry";
3497
+ var REVOKE_PATH = "/api/admin/network/admissions/revoke";
3498
+ var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
3499
+ var MAX_NOTE2 = 2e3;
3500
+ var reject4 = (status, code, error) => json({ ok: false, code, error }, status);
3501
+ async function gate2(req, env, ctx) {
3502
+ const db = ctx.makeDb(env);
3503
+ const user = await ctx.verifyUser(req, env);
3504
+ if (!user) return { response: reject4(401, "unauthorized", "unauthorized") };
3505
+ if (!await ctx.isAdmin(db, user)) return { response: reject4(403, "forbidden", "forbidden") };
3506
+ return { db, user };
3507
+ }
3508
+ function targetFor(ctx, id) {
3509
+ return ctx.chapter.network.targets.find((target) => target.id === id && target.admissionGrants);
3510
+ }
3511
+ async function deliverSaved(db, env, ctx, target, delivery) {
3512
+ if (delivery.status === "acknowledged") return delivery;
3513
+ const secret = await getVaultSecret(db, target.secretName);
3514
+ const attemptedAt = Date.now();
3515
+ const attempts = delivery.attempts + 1;
3516
+ let patch;
3517
+ if (!secret) {
3518
+ patch = {
3519
+ status: delivery.attempts > 0 ? "retrying" : "pending",
3520
+ attempts,
3521
+ lastAttemptAt: attemptedAt,
3522
+ lastError: "edge secret is missing",
3523
+ updatedAt: attemptedAt
3524
+ };
3525
+ } else {
3526
+ try {
3527
+ const receipt = await deliverAdmissionGrants({
3528
+ secret,
3529
+ sender: ctx.chapter.id,
3530
+ targetUrl: target.url,
3531
+ envelope: delivery.envelope,
3532
+ fetch: ((input, init) => fetchNetworkTarget(env, target, new URL(String(input)), init ?? {}))
3533
+ });
3534
+ patch = {
3535
+ status: "acknowledged",
3536
+ attempts,
3537
+ lastAttemptAt: attemptedAt,
3538
+ acknowledgedAt: receipt.acknowledgedAt,
3539
+ updatedAt: attemptedAt
3540
+ };
3541
+ } catch (error) {
3542
+ const permanent = error instanceof AdmissionGrantDeliveryError && error.status >= 400 && error.status < 500;
3543
+ patch = {
3544
+ status: permanent ? "rejected" : delivery.attempts > 0 ? "retrying" : "pending",
3545
+ attempts,
3546
+ lastAttemptAt: attemptedAt,
3547
+ lastError: error instanceof Error ? error.message.slice(0, 2e3) : "delivery failed",
3548
+ updatedAt: attemptedAt
3549
+ };
3550
+ }
3551
+ }
3552
+ const ops = [{
3553
+ t: "update",
3554
+ ns: "leaderAdmissionDeliveries",
3555
+ id: delivery.id,
3556
+ attrs: patch
3557
+ }];
3558
+ if (patch.status === "acknowledged" && delivery.lastError) {
3559
+ ops.push({ t: "retract", ns: "leaderAdmissionDeliveries", id: delivery.id, attrs: ["lastError"] });
3560
+ }
3561
+ await db.transact(ops, {
3562
+ mutationId: `leader-admission-attempt:${delivery.targetId}:${delivery.revision}:${attempts}:${patch.status}`
3563
+ });
3564
+ return { ...delivery, ...patch };
3565
+ }
3566
+ async function parseBody(req) {
3567
+ try {
3568
+ return await req.json();
3569
+ } catch {
3570
+ return null;
3571
+ }
3572
+ }
3573
+ var handleAdminNetworkAdmissions = async (req, url, env, ctx) => {
3574
+ if (![PATH, RETRY_PATH, REVOKE_PATH].includes(url.pathname)) return null;
3575
+ const auth = await gate2(req, env, ctx);
3576
+ if ("response" in auth) return auth.response;
3577
+ const db = auth.db;
3578
+ if (url.pathname === PATH && req.method === "GET") {
3579
+ const targetId2 = url.searchParams.get("targetId") ?? "";
3580
+ const target2 = targetFor(ctx, targetId2);
3581
+ if (!target2) return reject4(404, "unknown_target", "admission target is not configured");
3582
+ return json({
3583
+ ok: true,
3584
+ target: { id: target2.id, name: target2.name },
3585
+ assignments: await loadLeaderAdmissionAssignments(db, target2.id),
3586
+ delivery: await loadLeaderAdmissionDelivery(db, target2.id, env.ODLA_ENV)
3587
+ });
3588
+ }
3589
+ if (req.method !== "POST") return null;
3590
+ const body = await parseBody(req);
3591
+ if (!body) return reject4(422, "invalid_request", "request body must be JSON");
3592
+ const targetId = typeof body.targetId === "string" ? body.targetId : "";
3593
+ const target = targetFor(ctx, targetId);
3594
+ if (!target) return reject4(404, "unknown_target", "admission target is not configured");
3595
+ if (url.pathname === RETRY_PATH) {
3596
+ const delivery = await loadLeaderAdmissionDelivery(db, target.id, env.ODLA_ENV);
3597
+ if (!delivery) return reject4(404, "not_found", "no saved admission delivery exists");
3598
+ return json({ ok: true, delivery: await deliverSaved(db, env, ctx, target, delivery) });
3599
+ }
3600
+ const personId = typeof body.personId === "string" ? body.personId : "";
3601
+ const mutationId = typeof body.mutationId === "string" ? body.mutationId : "";
3602
+ if (!personId || !MUTATION_ID.test(mutationId)) {
3603
+ return reject4(422, "invalid_request", "personId and a stable mutationId are required");
3604
+ }
3605
+ const person = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: auth.db }, personId);
3606
+ if (!person) return reject4(404, "person_not_found", "person not found");
3607
+ if (!personAdmissionEmail(ctx, person)) {
3608
+ return reject4(422, "invalid_email", "person has no valid authoritative email");
3609
+ }
3610
+ const decision = body.decision ?? "waive_interview";
3611
+ if (decision !== "waive_interview" && decision !== "require_interview") {
3612
+ return reject4(422, "invalid_decision", "decision must be waive_interview or require_interview");
3613
+ }
3614
+ const note = typeof body.note === "string" ? body.note.trim() : "";
3615
+ if (note.length > MAX_NOTE2) return reject4(422, "invalid_note", "note is too long");
3616
+ const expiresAt = body.expiresAt == null ? void 0 : Number(body.expiresAt);
3617
+ if (expiresAt !== void 0 && (!Number.isFinite(expiresAt) || expiresAt <= Date.now())) {
3618
+ return reject4(422, "invalid_expiry", "expiresAt must be a future epoch timestamp");
3619
+ }
3620
+ try {
3621
+ const saved = await persistLeaderAdmissionDesiredSet({
3622
+ db,
3623
+ ctx,
3624
+ env,
3625
+ target,
3626
+ personId,
3627
+ decision,
3628
+ ...note ? { note } : {},
3629
+ ...expiresAt !== void 0 ? { expiresAt } : {},
3630
+ active: url.pathname !== REVOKE_PATH,
3631
+ mutationId,
3632
+ actor: auth.user.email ?? auth.user.userId
3633
+ });
3634
+ const delivery = await deliverSaved(db, env, ctx, target, saved.delivery);
3635
+ return json({ ok: true, duplicate: saved.duplicate, delivery }, delivery.status === "acknowledged" ? 200 : 202);
3636
+ } catch (error) {
3637
+ if (error instanceof Error && error.message === "mutation_conflict") {
3638
+ return reject4(409, "mutation_conflict", "mutationId was already used for another request");
3639
+ }
3640
+ if (error instanceof Error && /valid authoritative email/.test(error.message)) {
3641
+ return reject4(422, "invalid_email", error.message);
3642
+ }
3643
+ return reject4(409, "revision_conflict", "admission assignments changed; reload and retry");
3644
+ }
3645
+ };
3646
+ async function retryLeaderAdmissionDeliveries(env, ctx, limit = 100) {
3647
+ const db = ctx.makeDb(env);
3648
+ const result = await db.query({ leaderAdmissionDeliveries: { $: { limit } } });
3649
+ const pending = admissionRows(result.leaderAdmissionDeliveries).filter((delivery) => delivery.status !== "acknowledged" && delivery.status !== "rejected").slice(0, Math.max(1, Math.min(limit, 500)));
3650
+ const delivered = [];
3651
+ for (const item of pending) {
3652
+ const target = targetFor(ctx, item.targetId);
3653
+ if (target) delivered.push(await deliverSaved(db, env, ctx, target, item));
3654
+ }
3655
+ return delivered;
3656
+ }
3657
+
3287
3658
  // src/worker-routes-schedule.ts
3288
3659
  var import_calendar = require("@odla-ai/calendar");
3289
3660
  function errCode(err) {
@@ -4545,16 +4916,16 @@ var crmDeps2 = (db, ctx) => ({
4545
4916
  });
4546
4917
  var handleAdminCrmSync = async (req, url, env, ctx) => {
4547
4918
  if (req.method !== "POST" || url.pathname !== "/api/admin/crm/sync") return null;
4548
- const gate9 = await adminGate(req, env, ctx);
4549
- if (gate9 instanceof Response) return gate9;
4550
- const result = await backfillCrm(crmDeps2(gate9.db, ctx));
4919
+ const gate10 = await adminGate(req, env, ctx);
4920
+ if (gate10 instanceof Response) return gate10;
4921
+ const result = await backfillCrm(crmDeps2(gate10.db, ctx));
4551
4922
  return json({ ok: true, ...result });
4552
4923
  };
4553
4924
  var handleAdminPeople = async (req, url, env, ctx) => {
4554
4925
  if (req.method !== "GET" || url.pathname !== "/api/admin/people") return null;
4555
- const gate9 = await adminGate(req, env, ctx);
4556
- if (gate9 instanceof Response) return gate9;
4557
- const { db } = gate9;
4926
+ const gate10 = await adminGate(req, env, ctx);
4927
+ if (gate10 instanceof Response) return gate10;
4928
+ const { db } = gate10;
4558
4929
  const sk = await getVaultSecret(db, "clerk_secret_key");
4559
4930
  const [data, roleList] = await Promise.all([
4560
4931
  db.query({
@@ -4607,9 +4978,9 @@ var handleAdminPeople = async (req, url, env, ctx) => {
4607
4978
  };
4608
4979
  var handleAdminPeopleAccess = async (req, url, env, ctx) => {
4609
4980
  if (req.method !== "GET" || url.pathname !== "/api/admin/people/access") return null;
4610
- const gate9 = await adminGate(req, env, ctx);
4611
- if (gate9 instanceof Response) return gate9;
4612
- const { db } = gate9;
4981
+ const gate10 = await adminGate(req, env, ctx);
4982
+ if (gate10 instanceof Response) return gate10;
4983
+ const { db } = gate10;
4613
4984
  const targetId = url.searchParams.get("userId") ?? "";
4614
4985
  if (!targetId.startsWith("user_")) return json({ error: "invalid userId" }, 400);
4615
4986
  const sk = await getVaultSecret(db, "clerk_secret_key");
@@ -4620,9 +4991,9 @@ var handleAdminPeopleAccess = async (req, url, env, ctx) => {
4620
4991
  };
4621
4992
  var handleAdminPeopleRole = async (req, url, env, ctx) => {
4622
4993
  if (req.method !== "POST" || url.pathname !== "/api/admin/people/role") return null;
4623
- const gate9 = await adminGate(req, env, ctx);
4624
- if (gate9 instanceof Response) return gate9;
4625
- const { db, actor } = gate9;
4994
+ const gate10 = await adminGate(req, env, ctx);
4995
+ if (gate10 instanceof Response) return gate10;
4996
+ const { db, actor } = gate10;
4626
4997
  let body;
4627
4998
  try {
4628
4999
  body = await req.json();
@@ -4769,7 +5140,7 @@ function subAnnualCents(sub) {
4769
5140
  }
4770
5141
 
4771
5142
  // src/worker-routes-admin-dashboard.ts
4772
- async function gate2(req, env, ctx) {
5143
+ async function gate3(req, env, ctx) {
4773
5144
  const rawDb = ctx.makeDb(env);
4774
5145
  const u = await ctx.verifyUser(req, env);
4775
5146
  if (!u) return json({ error: "unauthorized" }, 401);
@@ -4779,7 +5150,7 @@ async function gate2(req, env, ctx) {
4779
5150
  var rows3 = (v) => Array.isArray(v) ? v : [];
4780
5151
  var handleAdminDashboard = async (req, url, env, ctx) => {
4781
5152
  if (req.method !== "GET" || url.pathname !== "/api/admin/dashboard") return null;
4782
- const got = await gate2(req, env, ctx);
5153
+ const got = await gate3(req, env, ctx);
4783
5154
  if (got instanceof Response) return got;
4784
5155
  const db = got;
4785
5156
  const now = Date.now();
@@ -4862,7 +5233,7 @@ var handleAdminDashboard = async (req, url, env, ctx) => {
4862
5233
  };
4863
5234
  var handleAdminBilling = async (req, url, env, ctx) => {
4864
5235
  if (req.method !== "GET" || url.pathname !== "/api/admin/billing") return null;
4865
- const got = await gate2(req, env, ctx);
5236
+ const got = await gate3(req, env, ctx);
4866
5237
  if (got instanceof Response) return got;
4867
5238
  const db = got;
4868
5239
  const sk = await getVaultSecret(db, "stripe_secret_key");
@@ -4922,7 +5293,7 @@ var handleAdminBilling = async (req, url, env, ctx) => {
4922
5293
 
4923
5294
  // src/worker-routes-admin-lifecycle.ts
4924
5295
  var import_calendar3 = require("@odla-ai/calendar");
4925
- async function gate3(req, env, ctx) {
5296
+ async function gate4(req, env, ctx) {
4926
5297
  const rawDb = ctx.makeDb(env);
4927
5298
  const u = await ctx.verifyUser(req, env);
4928
5299
  if (!u) return json({ error: "unauthorized" }, 401);
@@ -4948,7 +5319,7 @@ async function loadGroup(db, id) {
4948
5319
  var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
4949
5320
  const m = url.pathname.match(/^\/api\/admin\/meetings\/([0-9a-f-]+)\/reschedule$/);
4950
5321
  if (req.method !== "POST" || !m) return null;
4951
- const got = await gate3(req, env, ctx);
5322
+ const got = await gate4(req, env, ctx);
4952
5323
  if (got instanceof Response) return got;
4953
5324
  const db = got;
4954
5325
  const body = await readJson(req);
@@ -4989,7 +5360,7 @@ var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
4989
5360
  var handleAdminMeetingCancel = async (req, url, env, ctx) => {
4990
5361
  const m = url.pathname.match(/^\/api\/admin\/meetings\/([0-9a-f-]+)\/cancel$/);
4991
5362
  if (req.method !== "POST" || !m) return null;
4992
- const got = await gate3(req, env, ctx);
5363
+ const got = await gate4(req, env, ctx);
4993
5364
  if (got instanceof Response) return got;
4994
5365
  const db = got;
4995
5366
  const { meetings } = await db.query({ meetings: { $: { where: { id: m[1] }, limit: 1 } } });
@@ -5012,7 +5383,7 @@ var handleAdminMeetingCancel = async (req, url, env, ctx) => {
5012
5383
  var handleAdminApprove = async (req, url, env, ctx) => {
5013
5384
  const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/approve$/);
5014
5385
  if (req.method !== "POST" || !m) return null;
5015
- const got = await gate3(req, env, ctx);
5386
+ const got = await gate4(req, env, ctx);
5016
5387
  if (got instanceof Response) return got;
5017
5388
  const db = got;
5018
5389
  const id = m[1];
@@ -5025,7 +5396,7 @@ var handleAdminApprove = async (req, url, env, ctx) => {
5025
5396
  var handleAdminRefund = async (req, url, env, ctx) => {
5026
5397
  const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)\/refund$/);
5027
5398
  if (req.method !== "POST" || !m) return null;
5028
- const got = await gate3(req, env, ctx);
5399
+ const got = await gate4(req, env, ctx);
5029
5400
  if (got instanceof Response) return got;
5030
5401
  const db = got;
5031
5402
  const app = await loadApp(db, m[1]);
@@ -5057,7 +5428,7 @@ var handleAdminRefund = async (req, url, env, ctx) => {
5057
5428
  var handleAdminApplicationPatch = async (req, url, env, ctx) => {
5058
5429
  const m = url.pathname.match(/^\/api\/admin\/applications\/([0-9a-f-]+)$/);
5059
5430
  if (req.method !== "PATCH" || !m) return null;
5060
- const got = await gate3(req, env, ctx);
5431
+ const got = await gate4(req, env, ctx);
5061
5432
  if (got instanceof Response) return got;
5062
5433
  const db = got;
5063
5434
  const id = m[1];
@@ -5085,7 +5456,7 @@ var handleAdminApplicationPatch = async (req, url, env, ctx) => {
5085
5456
  };
5086
5457
 
5087
5458
  // src/worker-routes-admin-comms.ts
5088
- async function gate4(req, env, ctx) {
5459
+ async function gate5(req, env, ctx) {
5089
5460
  const rawDb = ctx.makeDb(env);
5090
5461
  const u = await ctx.verifyUser(req, env);
5091
5462
  if (!u) return json({ error: "unauthorized" }, 401);
@@ -5107,7 +5478,7 @@ var notifyDeps = (db, env) => ({
5107
5478
  });
5108
5479
  var handleAdminGroupEmail = async (req, url, env, ctx) => {
5109
5480
  if (url.pathname !== "/api/admin/group/email" || req.method !== "GET" && req.method !== "PUT") return null;
5110
- const got = await gate4(req, env, ctx);
5481
+ const got = await gate5(req, env, ctx);
5111
5482
  if (got instanceof Response) return got;
5112
5483
  const db = got;
5113
5484
  const group = await loadGroup2(db, ctx.chapter.id);
@@ -5169,7 +5540,7 @@ var handleAdminGroupEmail = async (req, url, env, ctx) => {
5169
5540
  };
5170
5541
  var handleAdminEmailLog = async (req, url, env, ctx) => {
5171
5542
  if (req.method !== "GET" || url.pathname !== "/api/admin/email/log") return null;
5172
- const got = await gate4(req, env, ctx);
5543
+ const got = await gate5(req, env, ctx);
5173
5544
  if (got instanceof Response) return got;
5174
5545
  const { emailLog } = await got.query({ emailLog: { $: { order: { sentAt: "desc" }, limit: 50 } } });
5175
5546
  const sends = (emailLog ?? []).map((r) => ({
@@ -5186,7 +5557,7 @@ var handleAdminEmailLog = async (req, url, env, ctx) => {
5186
5557
  };
5187
5558
  var handleAdminEmailTest = async (req, url, env, ctx) => {
5188
5559
  if (req.method !== "POST" || url.pathname !== "/api/admin/email/test") return null;
5189
- const got = await gate4(req, env, ctx);
5560
+ const got = await gate5(req, env, ctx);
5190
5561
  if (got instanceof Response) return got;
5191
5562
  const db = got;
5192
5563
  let body;
@@ -5214,7 +5585,7 @@ var handleAdminEmailTest = async (req, url, env, ctx) => {
5214
5585
  var handleAdminComms = async (req, url, env, ctx) => {
5215
5586
  const m = url.pathname.match(/^\/api\/admin\/people\/([0-9a-fA-F-]+)\/comms$/);
5216
5587
  if (req.method !== "GET" || !m) return null;
5217
- const got = await gate4(req, env, ctx);
5588
+ const got = await gate5(req, env, ctx);
5218
5589
  if (got instanceof Response) return got;
5219
5590
  const db = got;
5220
5591
  const appId = m[1];
@@ -5252,26 +5623,12 @@ var handleAdminComms = async (req, url, env, ctx) => {
5252
5623
  };
5253
5624
 
5254
5625
  // src/worker-routes-network.ts
5255
- var import_crm6 = require("@odla-ai/crm");
5256
- var import_db7 = require("@odla-ai/db");
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
- }
5626
+ var import_crm8 = require("@odla-ai/crm");
5627
+ var import_db8 = require("@odla-ai/db");
5271
5628
 
5272
5629
  // src/worker-network-push.ts
5273
- var import_crm5 = require("@odla-ai/crm");
5274
- var import_db6 = require("@odla-ai/db");
5630
+ var import_crm7 = require("@odla-ai/crm");
5631
+ var import_db7 = require("@odla-ai/db");
5275
5632
  async function pushNetworkRecord(db, env, ctx, target, record4) {
5276
5633
  const secret = await getVaultSecret(db, target.secretName);
5277
5634
  const crmDeps3 = {
@@ -5282,7 +5639,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
5282
5639
  };
5283
5640
  if (!secret) {
5284
5641
  const error = `vault secret "${target.secretName}" is missing`;
5285
- await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
5642
+ await (0, import_crm7.recordDeliveryAttempt)(crmDeps3, {
5286
5643
  recordId: record4.id,
5287
5644
  targetId: target.id,
5288
5645
  status: "failed",
@@ -5305,7 +5662,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
5305
5662
  const payloadBody = JSON.stringify(payload);
5306
5663
  const destination = new URL("/api/network/shared", target.url);
5307
5664
  try {
5308
- const signed = await (0, import_db6.signFederatedRequest)({
5665
+ const signed = await (0, import_db7.signFederatedRequest)({
5309
5666
  secret,
5310
5667
  sender: ctx.chapter.id,
5311
5668
  method: "POST",
@@ -5321,7 +5678,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
5321
5678
  const responseBody = await res.json().catch(() => ({}));
5322
5679
  if (!res.ok) {
5323
5680
  const error = responseBody.error ?? "follower rejected the record";
5324
- await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
5681
+ await (0, import_crm7.recordDeliveryAttempt)(crmDeps3, {
5325
5682
  recordId: record4.id,
5326
5683
  targetId: target.id,
5327
5684
  status: "failed",
@@ -5336,7 +5693,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
5336
5693
  error
5337
5694
  };
5338
5695
  }
5339
- await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
5696
+ await (0, import_crm7.recordDeliveryAttempt)(crmDeps3, {
5340
5697
  recordId: record4.id,
5341
5698
  targetId: target.id,
5342
5699
  status: "delivered",
@@ -5352,7 +5709,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
5352
5709
  };
5353
5710
  } catch (err) {
5354
5711
  const error = err instanceof Error ? err.message : "delivery failed";
5355
- await (0, import_crm5.recordDeliveryAttempt)(crmDeps3, {
5712
+ await (0, import_crm7.recordDeliveryAttempt)(crmDeps3, {
5356
5713
  recordId: record4.id,
5357
5714
  targetId: target.id,
5358
5715
  status: "failed",
@@ -5364,7 +5721,7 @@ async function pushNetworkRecord(db, env, ctx, target, record4) {
5364
5721
  }
5365
5722
 
5366
5723
  // src/worker-routes-network.ts
5367
- async function gate5(req, env, ctx) {
5724
+ async function gate6(req, env, ctx) {
5368
5725
  const db = ctx.makeDb(env);
5369
5726
  const user = await ctx.verifyUser(req, env);
5370
5727
  if (!user) return { response: json({ error: "unauthorized" }, 401) };
@@ -5373,15 +5730,25 @@ async function gate5(req, env, ctx) {
5373
5730
  }
5374
5731
  var handleAdminNetworkTargets = async (req, url, env, ctx) => {
5375
5732
  if (req.method !== "GET" || url.pathname !== "/api/admin/network/targets") return null;
5376
- const got = await gate5(req, env, ctx);
5733
+ const got = await gate6(req, env, ctx);
5377
5734
  if ("response" in got) return got.response;
5378
5735
  return json({
5379
- targets: ctx.chapter.network.targets.map(({ id, name, url: targetUrl, fields, sharedNotes }) => ({
5736
+ targets: ctx.chapter.network.targets.map(({
5737
+ id,
5738
+ name,
5739
+ url: targetUrl,
5740
+ fields,
5741
+ sharedNotes,
5742
+ editableFields,
5743
+ stageTransitions
5744
+ }) => ({
5380
5745
  id,
5381
5746
  name,
5382
5747
  url: targetUrl,
5383
5748
  types: Object.keys(fields ?? DEFAULT_SHARE_FIELDS),
5384
- sharedNotes
5749
+ sharedNotes,
5750
+ editableFields,
5751
+ stageTransitions
5385
5752
  }))
5386
5753
  });
5387
5754
  };
@@ -5390,10 +5757,10 @@ var handleNetworkSnapshot = async (req, url, env, ctx) => {
5390
5757
  const db = ctx.makeDb(env);
5391
5758
  const secret = await getVaultSecret(db, "network_share_secret");
5392
5759
  if (!secret) return json({ error: "unauthorized" }, 401);
5393
- const verified = await (0, import_db7.verifyFederatedRequest)(req, { secret });
5760
+ const verified = await (0, import_db8.verifyFederatedRequest)(req, { secret });
5394
5761
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
5395
5762
  const types = await Promise.all(
5396
- Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm6.typeSummary)({ crm: ctx.chapter.crm, db }, type))
5763
+ Object.keys(ctx.chapter.crm.config.types).map((type) => (0, import_crm8.typeSummary)({ crm: ctx.chapter.crm, db }, type))
5397
5764
  );
5398
5765
  const snapshot = {
5399
5766
  version: 1,
@@ -5408,7 +5775,7 @@ async function snapshotOne(db, env, ctx, target) {
5408
5775
  if (!secret) return { id: target.id, name: target.name, url: target.url, available: false, error: "edge secret is missing" };
5409
5776
  const destination = new URL("/api/network/snapshot", target.url);
5410
5777
  try {
5411
- const headers = await (0, import_db7.signFederatedRequest)({
5778
+ const headers = await (0, import_db8.signFederatedRequest)({
5412
5779
  secret,
5413
5780
  sender: ctx.chapter.id,
5414
5781
  method: "GET",
@@ -5449,7 +5816,7 @@ async function snapshotOne(db, env, ctx, target) {
5449
5816
  }
5450
5817
  var handleAdminNetworkRollup = async (req, url, env, ctx) => {
5451
5818
  if (req.method !== "GET" || url.pathname !== "/api/admin/network/rollup") return null;
5452
- const got = await gate5(req, env, ctx);
5819
+ const got = await gate6(req, env, ctx);
5453
5820
  if ("response" in got) return got.response;
5454
5821
  const targets = await Promise.all(
5455
5822
  ctx.chapter.network.targets.map((target) => snapshotOne(got.db, env, ctx, target))
@@ -5476,7 +5843,7 @@ var handleAdminNetworkRollup = async (req, url, env, ctx) => {
5476
5843
  };
5477
5844
  var handleAdminNetworkPush = async (req, url, env, ctx) => {
5478
5845
  if (req.method !== "POST" || url.pathname !== "/api/admin/network/push") return null;
5479
- const got = await gate5(req, env, ctx);
5846
+ const got = await gate6(req, env, ctx);
5480
5847
  if ("response" in got) return got.response;
5481
5848
  let body;
5482
5849
  try {
@@ -5491,7 +5858,7 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
5491
5858
  if (requested.size !== body.targetIds.length) return json({ error: "targetIds must contain unique strings" }, 400);
5492
5859
  const targets = ctx.chapter.network.targets.filter((target) => requested.has(target.id));
5493
5860
  if (targets.length !== requested.size) return json({ error: "one or more targetIds are not configured" }, 400);
5494
- const record4 = await (0, import_crm6.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
5861
+ const record4 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
5495
5862
  if (!record4) return json({ error: "record not found" }, 404);
5496
5863
  const results = await Promise.all(
5497
5864
  targets.map((target) => pushNetworkRecord(got.db, env, ctx, target, record4))
@@ -5499,19 +5866,20 @@ var handleAdminNetworkPush = async (req, url, env, ctx) => {
5499
5866
  return json({ ok: results.every((result) => result.ok), results });
5500
5867
  };
5501
5868
 
5502
- // src/worker-routes-network-records.ts
5503
- var import_crm7 = require("@odla-ai/crm");
5504
- var import_db8 = require("@odla-ai/db");
5869
+ // src/worker-routes-network-records-follower.ts
5870
+ var import_crm9 = require("@odla-ai/crm");
5871
+ var import_db9 = require("@odla-ai/db");
5872
+
5873
+ // src/worker-routes-network-records-common.ts
5505
5874
  var IDENTIFIER = /^[a-z][a-zA-Z0-9_]*$/;
5506
5875
  var MAX_PAGE = 50;
5507
5876
  var MAX_OFFSET = 1e4;
5508
5877
  var MAX_SEARCH = 200;
5509
- async function gate6(req, env, ctx) {
5510
- const db = ctx.makeDb(env);
5511
- const user = await ctx.verifyUser(req, env);
5512
- if (!user) return { response: json({ error: "unauthorized" }, 401) };
5513
- if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
5514
- return { db, user };
5878
+ var MAX_BODY = 32768;
5879
+ var RECORD_ID = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,191}$/;
5880
+ var MUTATION_ID2 = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{7,127}$/;
5881
+ function bodyTooLarge(body) {
5882
+ return new TextEncoder().encode(body).byteLength > MAX_BODY;
5515
5883
  }
5516
5884
  function integer(value, fallback, min, max) {
5517
5885
  if (value == null || value === "") return fallback;
@@ -5519,10 +5887,22 @@ function integer(value, fallback, min, max) {
5519
5887
  const parsed = Number(value);
5520
5888
  return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : null;
5521
5889
  }
5522
- function projectRecord(record4, allowed2) {
5890
+ function projectRecord(record4, allowed2, editable, stagesEnabled, crm) {
5523
5891
  const fields = Object.fromEntries(
5524
5892
  allowed2.flatMap((name) => Object.prototype.hasOwnProperty.call(record4.fields, name) ? [[name, record4.fields[name]]] : [])
5525
5893
  );
5894
+ const def = crm.type(record4.type);
5895
+ const editFields = editable.map((id) => {
5896
+ const field = def.fields[id];
5897
+ return {
5898
+ id,
5899
+ label: field.label ?? id,
5900
+ type: field.type,
5901
+ required: field.required ?? false,
5902
+ ...field.options ? { options: field.options } : {}
5903
+ };
5904
+ });
5905
+ const allowedStages = stagesEnabled ? (def.pipeline?.stages ?? []).flatMap((stage) => crm.canTransition(record4.type, record4, stage.id) ? [] : [{ id: stage.id, label: stage.label ?? stage.id }]) : [];
5526
5906
  return {
5527
5907
  id: record4.id,
5528
5908
  type: record4.type,
@@ -5530,23 +5910,175 @@ function projectRecord(record4, allowed2) {
5530
5910
  ...record4.stage ? { stage: record4.stage } : {},
5531
5911
  fields,
5532
5912
  createdAt: record4.createdAt,
5533
- updatedAt: record4.updatedAt
5913
+ updatedAt: record4.updatedAt,
5914
+ revision: record4.updatedAt,
5915
+ ...editFields.length || stagesEnabled ? { edit: { fields: editFields, allowedStages } } : {}
5534
5916
  };
5535
5917
  }
5918
+ function readerRecord(record4, reader, crm) {
5919
+ return projectRecord(
5920
+ record4,
5921
+ reader.fields[record4.type] ?? [],
5922
+ reader.editableFields[record4.type] ?? [],
5923
+ reader.stageTransitions.includes(record4.type),
5924
+ crm
5925
+ );
5926
+ }
5927
+ function restrictedRecord(record4, target) {
5928
+ if (!record4.edit) return record4;
5929
+ const fields = record4.edit.fields.filter((field) => target.editableFields[record4.type]?.includes(field.id));
5930
+ const allowedStages = target.stageTransitions.includes(record4.type) ? record4.edit.allowedStages : [];
5931
+ const edit = fields.length || target.stageTransitions.includes(record4.type) ? { fields, allowedStages } : void 0;
5932
+ return { ...record4, ...edit ? { edit } : { edit: void 0 } };
5933
+ }
5934
+
5935
+ // src/worker-routes-network-records-follower.ts
5936
+ var NETWORK_MUTATION_META = "chapterNetworkMutation";
5937
+ function mutationFingerprint(input) {
5938
+ const fields = input.fields ? Object.fromEntries(Object.entries(input.fields).sort(([a], [b]) => a.localeCompare(b))) : void 0;
5939
+ return JSON.stringify({
5940
+ recordId: input.recordId,
5941
+ type: input.type,
5942
+ expectedRevision: input.expectedRevision,
5943
+ expectedStage: input.expectedStage,
5944
+ mutationId: input.mutationId,
5945
+ fields,
5946
+ stage: input.stage,
5947
+ actor: input.actor
5948
+ });
5949
+ }
5950
+ function mutationError(error) {
5951
+ if (error?.code === "transact_guard_failed") {
5952
+ return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
5953
+ }
5954
+ if (error instanceof import_crm9.CrmInputError) {
5955
+ return json({ error: error.message, code: "invalid_transition_or_fields", ...error.fields ? { fields: error.fields } : {} }, 422);
5956
+ }
5957
+ return json({ error: "follower mutation failed", code: "mutation_failed" }, 500);
5958
+ }
5959
+ async function priorMutation(db, id) {
5960
+ const result = await db.query({
5961
+ [import_crm9.CRM_NS.activity]: { $: { where: { id }, limit: 1 } }
5962
+ });
5963
+ const row = (result[import_crm9.CRM_NS.activity] ?? [])[0];
5964
+ const meta = row?.meta;
5965
+ const receipt = meta?.[NETWORK_MUTATION_META];
5966
+ return typeof receipt?.fingerprint === "string" ? { fingerprint: receipt.fingerprint } : null;
5967
+ }
5536
5968
  var handleNetworkRecords = async (req, url, env, ctx) => {
5537
- if (req.method !== "GET" || url.pathname !== "/api/network/records") return null;
5969
+ if (url.pathname !== "/api/network/records" || req.method !== "GET" && req.method !== "PATCH") return null;
5970
+ if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
5971
+ return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
5972
+ }
5538
5973
  const db = ctx.makeDb(env);
5539
5974
  const secret = await getVaultSecret(db, "network_share_secret");
5540
5975
  const readers = ctx.chapter.network.readers;
5541
5976
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
5542
- const verified = await (0, import_db8.verifyFederatedRequest)(req, {
5977
+ const verified = await (0, import_db9.verifyFederatedRequest)(req, {
5543
5978
  secret,
5544
5979
  senders: readers.map((reader2) => reader2.id)
5545
5980
  });
5546
5981
  if (!verified.ok) return json({ error: "unauthorized", reason: verified.reason }, 401);
5547
5982
  const reader = readers.find((candidate) => candidate.id === verified.sender);
5983
+ if (!reader) return json({ error: "unauthorized" }, 401);
5984
+ if (req.method === "PATCH") {
5985
+ if (bodyTooLarge(verified.body)) {
5986
+ return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
5987
+ }
5988
+ let input;
5989
+ try {
5990
+ input = JSON.parse(verified.body);
5991
+ } catch {
5992
+ return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
5993
+ }
5994
+ const fields = input?.fields;
5995
+ const hasFields = Boolean(fields && typeof fields === "object" && !Array.isArray(fields));
5996
+ const hasStage = typeof input?.stage === "string";
5997
+ const actor = input?.actor;
5998
+ 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) {
5999
+ return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
6000
+ }
6001
+ const editable = reader.editableFields[input.type];
6002
+ if (hasFields) {
6003
+ const names = Object.keys(fields);
6004
+ if (!editable || names.length === 0 || names.length > 32 || names.some((name) => !editable.includes(name))) {
6005
+ return json({ error: "profile fields are not delegated for this reader", code: "field_forbidden" }, 403);
6006
+ }
6007
+ } else if (!reader.stageTransitions.includes(input.type)) {
6008
+ return json({ error: "stage transitions are not delegated for this reader", code: "stage_forbidden" }, 403);
6009
+ }
6010
+ const activityId = `network-edit:${verified.sender}:${input.mutationId}`;
6011
+ const fingerprint = mutationFingerprint(input);
6012
+ const prior = await priorMutation(db, activityId);
6013
+ if (prior) {
6014
+ if (prior.fingerprint !== fingerprint) {
6015
+ return json({ error: "mutationId was already used for different input", code: "mutation_conflict" }, 409);
6016
+ }
6017
+ const replayed = await (0, import_crm9.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
6018
+ if (!replayed || replayed.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
6019
+ const result = {
6020
+ version: 1,
6021
+ site: { id: ctx.chapter.id, name: ctx.chapter.name },
6022
+ record: readerRecord(replayed, reader, ctx.chapter.crm),
6023
+ duplicate: true
6024
+ };
6025
+ return json(result);
6026
+ }
6027
+ const record4 = await (0, import_crm9.getRecord)({ crm: ctx.chapter.crm, db }, input.recordId);
6028
+ if (!record4 || record4.type !== input.type) return json({ error: "record not found", code: "not_found" }, 404);
6029
+ if (record4.updatedAt !== input.expectedRevision) {
6030
+ return json({ error: "record changed since it was reviewed", code: "stale_revision" }, 409);
6031
+ }
6032
+ if (hasStage && (input.expectedStage ?? null) !== (record4.stage ?? null)) {
6033
+ return json({ error: "stage changed since it was reviewed", code: "stale_stage" }, 409);
6034
+ }
6035
+ const actorEmail = typeof actor.email === "string" && actor.email.length <= 320 ? actor.email : void 0;
6036
+ const auditMeta = {
6037
+ [NETWORK_MUTATION_META]: {
6038
+ version: 1,
6039
+ sourceId: verified.sender,
6040
+ actorId: actor.id,
6041
+ ...actorEmail ? { actorEmail } : {},
6042
+ mutationId: input.mutationId,
6043
+ fingerprint
6044
+ }
6045
+ };
6046
+ try {
6047
+ const deps = { crm: ctx.chapter.crm, db };
6048
+ const updated = hasFields ? await (0, import_crm9.updateRecord)(deps, {
6049
+ id: record4.id,
6050
+ input: fields,
6051
+ mutationId: activityId,
6052
+ expectedUpdatedAt: input.expectedRevision,
6053
+ audit: {
6054
+ id: activityId,
6055
+ body: `Profile edited remotely by ${verified.sender}`,
6056
+ authorId: `network:${verified.sender}`,
6057
+ meta: auditMeta
6058
+ }
6059
+ }) : await (0, import_crm9.setStage)(deps, {
6060
+ id: record4.id,
6061
+ to: input.stage,
6062
+ authorId: `network:${verified.sender}`,
6063
+ mutationId: activityId,
6064
+ expectedUpdatedAt: input.expectedRevision,
6065
+ expectedStage: input.expectedStage ?? null,
6066
+ activityId,
6067
+ activityMeta: auditMeta
6068
+ });
6069
+ const result = {
6070
+ version: 1,
6071
+ site: { id: ctx.chapter.id, name: ctx.chapter.name },
6072
+ record: readerRecord(updated.record, reader, ctx.chapter.crm),
6073
+ duplicate: updated.duplicate
6074
+ };
6075
+ return json(result);
6076
+ } catch (error) {
6077
+ return mutationError(error);
6078
+ }
6079
+ }
5548
6080
  const type = url.searchParams.get("type") ?? "";
5549
- const allowed2 = reader?.fields[type];
6081
+ const allowed2 = reader.fields[type];
5550
6082
  if (!IDENTIFIER.test(type) || !allowed2) return json({ error: "record type is not shared with this reader" }, 403);
5551
6083
  const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
5552
6084
  const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
@@ -5554,7 +6086,7 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
5554
6086
  if (limit == null || offset == null || search.length > MAX_SEARCH) {
5555
6087
  return json({ error: "invalid pagination or search parameters" }, 400);
5556
6088
  }
5557
- const page = await (0, import_crm7.listRecords)(
6089
+ const page = await (0, import_crm9.listRecords)(
5558
6090
  { crm: ctx.chapter.crm, db },
5559
6091
  {
5560
6092
  type,
@@ -5568,14 +6100,24 @@ var handleNetworkRecords = async (req, url, env, ctx) => {
5568
6100
  version: 1,
5569
6101
  site: { id: ctx.chapter.id, name: ctx.chapter.name },
5570
6102
  type,
5571
- records: page.records.map((record4) => projectRecord(record4, allowed2)),
6103
+ records: page.records.map((record4) => readerRecord(record4, reader, ctx.chapter.crm)),
5572
6104
  total: page.total,
5573
6105
  limit: page.limit,
5574
6106
  offset: page.offset
5575
6107
  };
5576
6108
  return json(response);
5577
6109
  };
5578
- function targetFor(ctx, id) {
6110
+
6111
+ // src/worker-routes-network-records-admin.ts
6112
+ var import_db10 = require("@odla-ai/db");
6113
+ async function gate7(req, env, ctx) {
6114
+ const db = ctx.makeDb(env);
6115
+ const user = await ctx.verifyUser(req, env);
6116
+ if (!user) return { response: json({ error: "unauthorized" }, 401) };
6117
+ if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
6118
+ return { db, user };
6119
+ }
6120
+ function targetFor2(ctx, id) {
5579
6121
  return ctx.chapter.network.targets.find((target) => target.id === id);
5580
6122
  }
5581
6123
  async function readTargetRecords(db, env, ctx, target, query) {
@@ -5584,12 +6126,7 @@ async function readTargetRecords(db, env, ctx, target, query) {
5584
6126
  const destination = new URL("/api/network/records", target.url);
5585
6127
  destination.search = query.toString();
5586
6128
  try {
5587
- const headers = await (0, import_db8.signFederatedRequest)({
5588
- secret,
5589
- sender: ctx.chapter.id,
5590
- method: "GET",
5591
- url: destination
5592
- });
6129
+ const headers = await (0, import_db10.signFederatedRequest)({ secret, sender: ctx.chapter.id, method: "GET", url: destination });
5593
6130
  const response = await fetchNetworkTarget(env, target, destination, {
5594
6131
  headers,
5595
6132
  signal: AbortSignal.timeout(1e4)
@@ -5602,18 +6139,93 @@ async function readTargetRecords(db, env, ctx, target, query) {
5602
6139
  error: upstream ?? (response.ok ? "invalid follower record response" : `follower returned ${response.status}`)
5603
6140
  };
5604
6141
  }
5605
- return { ok: true, page: body };
6142
+ return {
6143
+ ok: true,
6144
+ page: { ...body, records: body.records.map((record4) => restrictedRecord(record4, target)) }
6145
+ };
5606
6146
  } catch (error) {
5607
6147
  return { ok: false, error: error instanceof Error ? error.message : "record read failed" };
5608
6148
  }
5609
6149
  }
6150
+ async function mutateTargetRecord(db, env, ctx, target, input) {
6151
+ const fields = input.fields;
6152
+ if (fields) {
6153
+ const allowed2 = target.editableFields[input.type];
6154
+ if (!allowed2 || Object.keys(fields).some((name) => !allowed2.includes(name))) {
6155
+ return json({ error: "profile fields are not enabled for this target", code: "field_forbidden" }, 403);
6156
+ }
6157
+ } else if (!target.stageTransitions.includes(input.type)) {
6158
+ return json({ error: "stage transitions are not enabled for this target", code: "stage_forbidden" }, 403);
6159
+ }
6160
+ const secret = await getVaultSecret(db, target.secretName);
6161
+ if (!secret) return json({ error: "edge secret is missing", code: "edge_unavailable" }, 502);
6162
+ const destination = new URL("/api/network/records", target.url);
6163
+ const body = JSON.stringify(input);
6164
+ try {
6165
+ const headers = await (0, import_db10.signFederatedRequest)({
6166
+ secret,
6167
+ sender: ctx.chapter.id,
6168
+ method: "PATCH",
6169
+ url: destination,
6170
+ body
6171
+ });
6172
+ const response = await fetchNetworkTarget(env, target, destination, {
6173
+ method: "PATCH",
6174
+ headers: { ...headers, "content-type": "application/json" },
6175
+ body,
6176
+ signal: AbortSignal.timeout(1e4)
6177
+ });
6178
+ const payload = await response.json().catch(() => null);
6179
+ const site = payload && "site" in payload ? payload.site : void 0;
6180
+ if (!response.ok || !payload || !("record" in payload) || site && site.id !== target.id) {
6181
+ const status = [401, 403, 404, 409, 422].includes(response.status) ? response.status : 502;
6182
+ return json({
6183
+ error: payload && "error" in payload && typeof payload.error === "string" ? payload.error : `follower returned ${response.status}`,
6184
+ ...payload && "code" in payload && typeof payload.code === "string" ? { code: payload.code } : {}
6185
+ }, status);
6186
+ }
6187
+ return json({ ...payload, record: restrictedRecord(payload.record, target) });
6188
+ } catch {
6189
+ return json({ error: "follower mutation request failed", code: "follower_unavailable" }, 502);
6190
+ }
6191
+ }
5610
6192
  var handleAdminNetworkRecords = async (req, url, env, ctx) => {
5611
- if (req.method !== "GET" || url.pathname !== "/api/admin/network/records") return null;
5612
- const got = await gate6(req, env, ctx);
6193
+ if (url.pathname !== "/api/admin/network/records" || req.method !== "GET" && req.method !== "PATCH") return null;
6194
+ if (req.method === "PATCH" && Number(req.headers.get("content-length") ?? 0) > MAX_BODY) {
6195
+ return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
6196
+ }
6197
+ const got = await gate7(req, env, ctx);
5613
6198
  if ("response" in got) return got.response;
6199
+ if (req.method === "PATCH") {
6200
+ let input;
6201
+ try {
6202
+ const body = await req.text();
6203
+ if (bodyTooLarge(body)) {
6204
+ return json({ error: "mutation body is too large", code: "body_too_large" }, 413);
6205
+ }
6206
+ input = JSON.parse(body);
6207
+ } catch {
6208
+ return json({ error: "invalid JSON body", code: "invalid_json" }, 422);
6209
+ }
6210
+ const target2 = targetFor2(ctx, input?.targetId);
6211
+ const hasFields = Boolean(input?.fields && typeof input.fields === "object" && !Array.isArray(input.fields));
6212
+ const hasStage = typeof input?.stage === "string";
6213
+ 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) {
6214
+ return json({ error: "invalid network mutation", code: "invalid_mutation" }, 422);
6215
+ }
6216
+ return mutateTargetRecord(got.db, env, ctx, target2, {
6217
+ type: input.type,
6218
+ recordId: input.recordId,
6219
+ expectedRevision: input.expectedRevision,
6220
+ mutationId: input.mutationId,
6221
+ ...input.expectedStage !== void 0 ? { expectedStage: input.expectedStage } : {},
6222
+ ...hasFields ? { fields: input.fields } : { stage: input.stage },
6223
+ actor: { id: got.user.userId, ...got.user.email ? { email: got.user.email } : {} }
6224
+ });
6225
+ }
5614
6226
  const targetId = url.searchParams.get("targetId") ?? "";
5615
6227
  const type = url.searchParams.get("type") ?? "";
5616
- const target = targetFor(ctx, targetId);
6228
+ const target = targetFor2(ctx, targetId);
5617
6229
  if (!target || !IDENTIFIER.test(type)) return json({ error: "configured targetId and record type are required" }, 400);
5618
6230
  const limit = integer(url.searchParams.get("limit"), 25, 1, MAX_PAGE);
5619
6231
  const offset = integer(url.searchParams.get("offset"), 0, 0, MAX_OFFSET);
@@ -5623,21 +6235,15 @@ var handleAdminNetworkRecords = async (req, url, env, ctx) => {
5623
6235
  }
5624
6236
  const query = new URLSearchParams({ type, limit: String(limit), offset: String(offset) });
5625
6237
  if (search) query.set("search", search);
5626
- const result = await readTargetRecords(
5627
- got.db,
5628
- env,
5629
- ctx,
5630
- target,
5631
- query
5632
- );
6238
+ const result = await readTargetRecords(got.db, env, ctx, target, query);
5633
6239
  return result.ok ? json(result.page) : json({ error: result.error }, 502);
5634
6240
  };
5635
6241
 
5636
6242
  // src/worker-routes-network-notes.ts
5637
6243
  var IDENTIFIER2 = /^[a-z][a-zA-Z0-9_]*$/;
5638
- var RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
5639
- var MAX_NOTE2 = 4e3;
5640
- async function gate7(req, env, ctx) {
6244
+ var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
6245
+ var MAX_NOTE3 = 4e3;
6246
+ async function gate8(req, env, ctx) {
5641
6247
  const db = ctx.makeDb(env);
5642
6248
  const user = await ctx.verifyUser(req, env);
5643
6249
  if (!user) return { response: json({ error: "unauthorized" }, 401) };
@@ -5649,7 +6255,7 @@ function noteIdentity(url, ctx) {
5649
6255
  const recordType = url.searchParams.get("type") ?? "";
5650
6256
  const recordId = url.searchParams.get("recordId") ?? "";
5651
6257
  const configured = ctx.chapter.network.targets.some((target) => target.id === targetId);
5652
- if (!configured || !IDENTIFIER2.test(recordType) || !RECORD_ID.test(recordId)) return null;
6258
+ if (!configured || !IDENTIFIER2.test(recordType) || !RECORD_ID2.test(recordId)) return null;
5653
6259
  return {
5654
6260
  targetId,
5655
6261
  recordType,
@@ -5671,7 +6277,7 @@ function asNote(row) {
5671
6277
  }
5672
6278
  var handleAdminNetworkNotes = async (req, url, env, ctx) => {
5673
6279
  if (url.pathname !== "/api/admin/network/notes" || req.method !== "GET" && req.method !== "POST") return null;
5674
- const got = await gate7(req, env, ctx);
6280
+ const got = await gate8(req, env, ctx);
5675
6281
  if ("response" in got) return got.response;
5676
6282
  const identity = noteIdentity(url, ctx);
5677
6283
  if (!identity) return json({ error: "valid targetId, type, and recordId are required" }, 400);
@@ -5688,7 +6294,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
5688
6294
  return json({ notes: (result.networkNotes ?? []).map((row) => asNote(row)) });
5689
6295
  }
5690
6296
  const contentLength = Number(req.headers.get("content-length") ?? 0);
5691
- if (contentLength > MAX_NOTE2 * 2) return json({ error: "note is too large" }, 413);
6297
+ if (contentLength > MAX_NOTE3 * 2) return json({ error: "note is too large" }, 413);
5692
6298
  let input;
5693
6299
  try {
5694
6300
  input = await req.json();
@@ -5696,7 +6302,7 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
5696
6302
  return json({ error: "invalid JSON body" }, 400);
5697
6303
  }
5698
6304
  const body = typeof input.body === "string" ? input.body.trim() : "";
5699
- if (!body || body.length > MAX_NOTE2) return json({ error: `note must be 1\u2013${MAX_NOTE2} characters` }, 400);
6305
+ if (!body || body.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
5700
6306
  const id = crypto.randomUUID();
5701
6307
  const note = {
5702
6308
  id,
@@ -5718,14 +6324,14 @@ var handleAdminNetworkNotes = async (req, url, env, ctx) => {
5718
6324
  };
5719
6325
 
5720
6326
  // src/worker-routes-network-shared-notes.ts
5721
- var import_crm8 = require("@odla-ai/crm");
5722
- var import_db9 = require("@odla-ai/db");
6327
+ var import_crm10 = require("@odla-ai/crm");
6328
+ var import_db11 = require("@odla-ai/db");
5723
6329
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
5724
- var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
5725
- var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
5726
- var MAX_NOTE3 = 4e3;
6330
+ var RECORD_ID3 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
6331
+ var MUTATION_ID3 = /^[A-Za-z0-9_-]{16,128}$/;
6332
+ var MAX_NOTE4 = 4e3;
5727
6333
  var NETWORK_META = "chapterNetwork";
5728
- async function gate8(req, env, ctx) {
6334
+ async function gate9(req, env, ctx) {
5729
6335
  const db = ctx.makeDb(env);
5730
6336
  const user = await ctx.verifyUser(req, env);
5731
6337
  if (!user) return { response: json({ error: "unauthorized" }, 401) };
@@ -5748,7 +6354,7 @@ function sharedNote(activity, sourceId) {
5748
6354
  }
5749
6355
  async function listSharedNotes(db, sourceId, recordId) {
5750
6356
  const result = await db.query({
5751
- [import_crm8.CRM_NS.activity]: {
6357
+ [import_crm10.CRM_NS.activity]: {
5752
6358
  $: {
5753
6359
  where: {
5754
6360
  and: [
@@ -5761,7 +6367,7 @@ async function listSharedNotes(db, sourceId, recordId) {
5761
6367
  }
5762
6368
  }
5763
6369
  });
5764
- return (result[import_crm8.CRM_NS.activity] ?? []).flatMap((activity) => {
6370
+ return (result[import_crm10.CRM_NS.activity] ?? []).flatMap((activity) => {
5765
6371
  const note = sharedNote(activity, sourceId);
5766
6372
  return note ? [note] : [];
5767
6373
  });
@@ -5771,14 +6377,14 @@ function sourceLabel(sourceId) {
5771
6377
  }
5772
6378
  var handleNetworkSharedNotes = async (req, url, env, ctx) => {
5773
6379
  if (url.pathname !== "/api/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
5774
- if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_NOTE3 * 2) {
6380
+ if (req.method === "POST" && Number(req.headers.get("content-length") ?? 0) > MAX_NOTE4 * 2) {
5775
6381
  return json({ error: "note is too large" }, 413);
5776
6382
  }
5777
6383
  const db = ctx.makeDb(env);
5778
6384
  const secret = await getVaultSecret(db, "network_share_secret");
5779
6385
  const readers = ctx.chapter.network.readers;
5780
6386
  if (!secret || readers.length === 0) return json({ error: "unauthorized" }, 401);
5781
- const verified = await (0, import_db9.verifyFederatedRequest)(req, {
6387
+ const verified = await (0, import_db11.verifyFederatedRequest)(req, {
5782
6388
  secret,
5783
6389
  senders: readers.map((reader2) => reader2.id)
5784
6390
  });
@@ -5786,10 +6392,10 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
5786
6392
  const reader = readers.find((candidate) => candidate.id === verified.sender);
5787
6393
  const recordType = url.searchParams.get("type") ?? "";
5788
6394
  const recordId = url.searchParams.get("recordId") ?? "";
5789
- if (!IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
6395
+ if (!IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId) || !reader?.sharedNotes.includes(recordType)) {
5790
6396
  return json({ error: "shared notes are not enabled for this reader and record type" }, 403);
5791
6397
  }
5792
- const record4 = await (0, import_crm8.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
6398
+ const record4 = await (0, import_crm10.getRecord)({ crm: ctx.chapter.crm, db }, recordId);
5793
6399
  if (!record4 || record4.type !== recordType) return json({ error: "record not found" }, 404);
5794
6400
  if (req.method === "GET") {
5795
6401
  return json({
@@ -5806,12 +6412,12 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
5806
6412
  }
5807
6413
  const body = typeof input.body === "string" ? input.body.trim() : "";
5808
6414
  const mutationId = typeof input.mutationId === "string" ? input.mutationId : "";
5809
- if (!body || body.length > MAX_NOTE3 || !MUTATION_ID.test(mutationId)) {
5810
- return json({ error: `body must be 1\u2013${MAX_NOTE3} characters and mutationId must be valid` }, 400);
6415
+ if (!body || body.length > MAX_NOTE4 || !MUTATION_ID3.test(mutationId)) {
6416
+ return json({ error: `body must be 1\u2013${MAX_NOTE4} characters and mutationId must be valid` }, 400);
5811
6417
  }
5812
6418
  const createdAt = Date.now();
5813
6419
  const id = `network:${verified.sender}:${mutationId}`;
5814
- const result = await (0, import_crm8.addActivity)(
6420
+ const result = await (0, import_crm10.addActivity)(
5815
6421
  {
5816
6422
  crm: ctx.chapter.crm,
5817
6423
  db,
@@ -5844,18 +6450,18 @@ var handleNetworkSharedNotes = async (req, url, env, ctx) => {
5844
6450
  };
5845
6451
  return json({ version: 1, site: { id: ctx.chapter.id, name: ctx.chapter.name }, note, duplicate: result.duplicate }, 201);
5846
6452
  };
5847
- function targetFor2(ctx, id) {
6453
+ function targetFor3(ctx, id) {
5848
6454
  return ctx.chapter.network.targets.find((target) => target.id === id);
5849
6455
  }
5850
6456
  var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5851
6457
  if (url.pathname !== "/api/admin/network/shared-notes" || req.method !== "GET" && req.method !== "POST") return null;
5852
- const got = await gate8(req, env, ctx);
6458
+ const got = await gate9(req, env, ctx);
5853
6459
  if ("response" in got) return got.response;
5854
6460
  const targetId = url.searchParams.get("targetId") ?? "";
5855
6461
  const recordType = url.searchParams.get("type") ?? "";
5856
6462
  const recordId = url.searchParams.get("recordId") ?? "";
5857
- const target = targetFor2(ctx, targetId);
5858
- if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId)) {
6463
+ const target = targetFor3(ctx, targetId);
6464
+ if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID3.test(recordId)) {
5859
6465
  return json({ error: "configured targetId, type, and recordId are required" }, 400);
5860
6466
  }
5861
6467
  let body = "";
@@ -5867,7 +6473,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5867
6473
  return json({ error: "invalid JSON body" }, 400);
5868
6474
  }
5869
6475
  const note = typeof input.body === "string" ? input.body.trim() : "";
5870
- if (!note || note.length > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
6476
+ if (!note || note.length > MAX_NOTE4) return json({ error: `note must be 1\u2013${MAX_NOTE4} characters` }, 400);
5871
6477
  body = JSON.stringify({ body: note, mutationId: crypto.randomUUID() });
5872
6478
  }
5873
6479
  const secret = await getVaultSecret(got.db, target.secretName);
@@ -5875,7 +6481,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5875
6481
  const destination = new URL("/api/network/shared-notes", target.url);
5876
6482
  destination.search = new URLSearchParams({ type: recordType, recordId }).toString();
5877
6483
  try {
5878
- const headers = await (0, import_db9.signFederatedRequest)({
6484
+ const headers = await (0, import_db11.signFederatedRequest)({
5879
6485
  secret,
5880
6486
  sender: ctx.chapter.id,
5881
6487
  method: req.method,
@@ -5902,7 +6508,7 @@ var handleAdminNetworkSharedNotes = async (req, url, env, ctx) => {
5902
6508
  };
5903
6509
 
5904
6510
  // src/worker-routes-formation.ts
5905
- var import_crm9 = require("@odla-ai/crm");
6511
+ var import_crm11 = require("@odla-ai/crm");
5906
6512
  var SUBMISSION = /^[A-Za-z0-9_-]{8,128}$/;
5907
6513
  var handleFormation = async (req, url, env, ctx) => {
5908
6514
  const formation = ctx.chapter.formation;
@@ -5954,7 +6560,7 @@ var handleFormation = async (req, url, env, ctx) => {
5954
6560
  const db = ctx.makeDb(env);
5955
6561
  const id = submissionId ? await applicationIdForSubmission(`formation:${ctx.chapter.id}:${submissionId}`) : crypto.randomUUID();
5956
6562
  try {
5957
- const created = await (0, import_crm9.createRecord)(
6563
+ const created = await (0, import_crm11.createRecord)(
5958
6564
  {
5959
6565
  crm: ctx.chapter.crm,
5960
6566
  db,
@@ -5982,7 +6588,7 @@ var handleFormation = async (req, url, env, ctx) => {
5982
6588
  };
5983
6589
 
5984
6590
  // src/worker-routes-builder-library.ts
5985
- var import_db10 = require("@odla-ai/db");
6591
+ var import_db12 = require("@odla-ai/db");
5986
6592
 
5987
6593
  // src/builder-library-validation.ts
5988
6594
  var ID4 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -6265,7 +6871,7 @@ async function verifySource(req, db, ctx) {
6265
6871
  if (ctx.chapter.mode === "chapter") {
6266
6872
  const secret = await getVaultSecret(db, "network_share_secret");
6267
6873
  if (!secret) return null;
6268
- const verified = await (0, import_db10.verifyFederatedRequest)(req, {
6874
+ const verified = await (0, import_db12.verifyFederatedRequest)(req, {
6269
6875
  secret,
6270
6876
  senders: sources.map((source2) => source2.sourceId)
6271
6877
  });
@@ -6278,7 +6884,7 @@ async function verifySource(req, db, ctx) {
6278
6884
  if (!target) continue;
6279
6885
  const secret = await getVaultSecret(db, target.secretName);
6280
6886
  if (!secret) continue;
6281
- const verified = await (0, import_db10.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
6887
+ const verified = await (0, import_db12.verifyFederatedRequest)(req.clone(), { secret, senders: [source.sourceId] });
6282
6888
  if (verified.ok) return { source, body: verified.body };
6283
6889
  }
6284
6890
  return null;
@@ -6467,11 +7073,11 @@ var handleAdminBuilderLibrary = async (req, url, env, ctx) => {
6467
7073
  };
6468
7074
 
6469
7075
  // src/signup-control-delivery.ts
6470
- var import_db11 = require("@odla-ai/db");
7076
+ var import_db13 = require("@odla-ai/db");
6471
7077
  async function deliverSignupControl(options) {
6472
7078
  const destination = new URL(SIGNUP_CONTROL_PATH, options.targetUrl);
6473
7079
  const body = JSON.stringify(options.envelope);
6474
- const signed = await (0, import_db11.signFederatedRequest)({
7080
+ const signed = await (0, import_db13.signFederatedRequest)({
6475
7081
  secret: options.secret,
6476
7082
  sender: options.sender,
6477
7083
  method: "POST",
@@ -6492,11 +7098,11 @@ async function deliverSignupControl(options) {
6492
7098
  }
6493
7099
 
6494
7100
  // src/member-content-delivery.ts
6495
- var import_db12 = require("@odla-ai/db");
7101
+ var import_db14 = require("@odla-ai/db");
6496
7102
  async function deliverMemberContent(options) {
6497
7103
  const destination = new URL(MEMBER_CONTENT_CONTROL_PATH, options.targetUrl);
6498
7104
  const body = JSON.stringify(options.envelope);
6499
- const signed = await (0, import_db12.signFederatedRequest)({
7105
+ const signed = await (0, import_db14.signFederatedRequest)({
6500
7106
  secret: options.secret,
6501
7107
  sender: options.sender,
6502
7108
  method: "POST",
@@ -6517,11 +7123,11 @@ async function deliverMemberContent(options) {
6517
7123
  }
6518
7124
 
6519
7125
  // src/builder-library-delivery.ts
6520
- var import_db13 = require("@odla-ai/db");
7126
+ var import_db15 = require("@odla-ai/db");
6521
7127
  async function deliverBuilderLibrary(options) {
6522
7128
  const destination = new URL(BUILDER_LIBRARY_CONTROL_PATH, options.targetUrl);
6523
7129
  const body = JSON.stringify(options.envelope);
6524
- const signed = await (0, import_db13.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
7130
+ const signed = await (0, import_db15.signFederatedRequest)({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
6525
7131
  const response = await (options.fetch ?? globalThis.fetch)(destination, { method: "POST", headers: { ...signed, "content-type": "application/json" }, body });
6526
7132
  const result = await response.json();
6527
7133
  if (!response.ok || !("ok" in result) || result.ok !== true) {
@@ -6531,31 +7137,6 @@ async function deliverBuilderLibrary(options) {
6531
7137
  return result;
6532
7138
  }
6533
7139
 
6534
- // src/admission-grants-delivery.ts
6535
- var import_db14 = require("@odla-ai/db");
6536
- async function deliverAdmissionGrants(options) {
6537
- const destination = new URL(ADMISSION_GRANTS_PATH, options.targetUrl);
6538
- const body = JSON.stringify(options.envelope);
6539
- const signed = await (0, import_db14.signFederatedRequest)({
6540
- secret: options.secret,
6541
- sender: options.sender,
6542
- method: "POST",
6543
- url: destination,
6544
- body
6545
- });
6546
- const response = await (options.fetch ?? globalThis.fetch)(destination, {
6547
- method: "POST",
6548
- headers: { ...signed, "content-type": "application/json" },
6549
- body
6550
- });
6551
- const result = await response.json();
6552
- if (!response.ok || !("ok" in result) || result.ok !== true) {
6553
- const failure = result;
6554
- throw new Error(`admission grants rejected (${response.status}): ${failure.reason ?? failure.error ?? "unknown"}`);
6555
- }
6556
- return result;
6557
- }
6558
-
6559
7140
  // src/signup-control-theme.ts
6560
7141
  var SIGNUP_CONTROL_THEME_MARKER = "<!--odla:signup-control-theme-->";
6561
7142
  var SIGNUP_CONTROL_THEME_STYLE_ID = "odla-signup-control-theme";
@@ -6715,6 +7296,7 @@ var BUILTIN_ROUTES = [
6715
7296
  handleAdminPeopleRole,
6716
7297
  handleAdminCrmSync,
6717
7298
  handleAdminAdmissionGrants,
7299
+ handleAdminNetworkAdmissions,
6718
7300
  handleAdminClerkPrivateProfileSync,
6719
7301
  // Aggregation
6720
7302
  handleAdminDashboard,