@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.
@@ -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 authority = ctx.chapter.signupControl;
3114
- if (!authority) return json({ error: "not found" }, 404);
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(db, authority.secretName);
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: [authority.sourceId] });
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 !== authority.sourceId) return reject3("wrong_source", 403);
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, authority.sourceId, ctx.chapter.id, env.ODLA_ENV);
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 gate9 = await adminGate(req, env, ctx);
4521
- if (gate9 instanceof Response) return gate9;
4522
- const result = await backfillCrm(crmDeps2(gate9.db, ctx));
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 gate9 = await adminGate(req, env, ctx);
4528
- if (gate9 instanceof Response) return gate9;
4529
- const { db } = gate9;
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 gate9 = await adminGate(req, env, ctx);
4583
- if (gate9 instanceof Response) return gate9;
4584
- const { db } = gate9;
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 gate9 = await adminGate(req, env, ctx);
4596
- if (gate9 instanceof Response) return gate9;
4597
- const { db, actor } = gate9;
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 gate2(req, env, ctx) {
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 gate2(req, env, ctx);
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 gate2(req, env, ctx);
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 gate3(req, env, ctx) {
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 gate3(req, env, ctx);
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 gate3(req, env, ctx);
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 gate3(req, env, ctx);
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 gate3(req, env, ctx);
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 gate3(req, env, ctx);
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 gate4(req, env, ctx) {
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 gate4(req, env, ctx);
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 gate4(req, env, ctx);
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 gate4(req, env, ctx);
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 gate4(req, env, ctx);
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 signFederatedRequest2, verifyFederatedRequest as verifyFederatedRequest5 } from "@odla-ai/db";
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 signFederatedRequest({
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 gate5(req, env, ctx) {
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 gate5(req, env, ctx);
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(({ id, name, url: targetUrl, fields, sharedNotes }) => ({
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 signFederatedRequest2({
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 gate5(req, env, ctx);
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 gate5(req, env, ctx);
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 getRecord({ crm: ctx.chapter.crm, db: got.db }, body.recordId);
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 { signFederatedRequest as signFederatedRequest3, verifyFederatedRequest as verifyFederatedRequest6 } from "@odla-ai/db";
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
- async function gate6(req, env, ctx) {
5482
- const db = ctx.makeDb(env);
5483
- const user = await ctx.verifyUser(req, env);
5484
- if (!user) return { response: json({ error: "unauthorized" }, 401) };
5485
- if (!await ctx.isAdmin(db, user)) return { response: json({ error: "forbidden" }, 403) };
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" || url.pathname !== "/api/network/records") return null;
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?.fields[type];
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) => projectRecord(record4, allowed2)),
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
- function targetFor(ctx, id) {
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 signFederatedRequest3({
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 { ok: true, page: body };
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" || url.pathname !== "/api/admin/network/records") return null;
5584
- const got = await gate6(req, env, ctx);
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 = targetFor(ctx, targetId);
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 RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
5611
- var MAX_NOTE2 = 4e3;
5612
- async function gate7(req, env, ctx) {
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) || !RECORD_ID.test(recordId)) return null;
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 gate7(req, env, ctx);
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 > MAX_NOTE2 * 2) return json({ error: "note is too large" }, 413);
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 > MAX_NOTE2) return json({ error: `note must be 1\u2013${MAX_NOTE2} characters` }, 400);
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 getRecord2
6299
+ CRM_NS as CRM_NS2,
6300
+ getRecord as getRecord5
5697
6301
  } from "@odla-ai/crm";
5698
- import { signFederatedRequest as signFederatedRequest4, verifyFederatedRequest as verifyFederatedRequest7 } from "@odla-ai/db";
6302
+ import { signFederatedRequest as signFederatedRequest5, verifyFederatedRequest as verifyFederatedRequest7 } from "@odla-ai/db";
5699
6303
  var IDENTIFIER3 = /^[a-z][a-zA-Z0-9_]*$/;
5700
- var RECORD_ID2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
5701
- var MUTATION_ID = /^[A-Za-z0-9_-]{16,128}$/;
5702
- var MAX_NOTE3 = 4e3;
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 gate8(req, env, ctx) {
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
- [CRM_NS.activity]: {
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[CRM_NS.activity] ?? []).flatMap((activity) => {
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) > MAX_NOTE3 * 2) {
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) || !RECORD_ID2.test(recordId) || !reader?.sharedNotes.includes(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 getRecord2({ crm: ctx.chapter.crm, db }, recordId);
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 > MAX_NOTE3 || !MUTATION_ID.test(mutationId)) {
5786
- return json({ error: `body must be 1\u2013${MAX_NOTE3} characters and mutationId must be valid` }, 400);
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 targetFor2(ctx, id) {
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 gate8(req, env, ctx);
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 = targetFor2(ctx, targetId);
5834
- if (!target || !target.sharedNotes.includes(recordType) || !IDENTIFIER3.test(recordType) || !RECORD_ID2.test(recordId)) {
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 > MAX_NOTE3) return json({ error: `note must be 1\u2013${MAX_NOTE3} characters` }, 400);
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 signFederatedRequest4({
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 signFederatedRequest5 } from "@odla-ai/db";
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 signFederatedRequest5({
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 signFederatedRequest6 } from "@odla-ai/db";
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 signFederatedRequest6({
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 signFederatedRequest7 } from "@odla-ai/db";
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 signFederatedRequest7({ secret: options.secret, sender: options.sender, method: "POST", url: destination, body });
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