// The Cloudflare Worker that fronts the R2 managed session-backup bucket.
//
// SECURITY: like agents-traces (and UNLIKE agents-share), there is NO public GET
// path here. Every route — PUT, GET, LIST, DELETE — requires a Phoenix bearer;
// there is NO static-token principal (the zero-knowledge BYO path talks to the
// user's own R2 bucket directly and never reaches this Worker, so a WRITE_TOKEN
// here would only be a Phoenix-and-quota bypass — PHNX-3726). The caller must be
// the record's owner: the path's first segment MUST equal the verified userId. A
// missing bearer → 401; a wrong owner → 403; public cache headers are NEVER
// emitted. A stored transcript body MUST be an AES-256-GCM envelope — a plaintext
// PUT is rejected 422 so readable transcript content can never land in the bucket.
//
// Object layout is OPAQUE to the Worker: the CLI stores each session under
//   <userId>/sessions/<machine>/<agent>/<sessionId>.jsonl          (file-shaped)
//   <userId>/sessions/<machine>/<agent>/<sessionId>/<relKey>       (dir-shaped)
// but the Worker only enforces segments[0] === userId and never parses the rest.
// Deliberately DIFFERENT from traces: there is NO `/all` cross-device merge here.
// Session transcripts are opaque, client-side-encrypted envelopes — merging or
// re-projecting them server-side would corrupt them — so the Worker is a pure
// per-owner blob store with a server-side LIST for restore.
//
// Reserved prefixes `__usage` (the CAS quota ledger) and `__key` (the escrowed
// backup DEK) are owner-bearer only and are NEVER returned by LIST.
//
// Routes (all require a Phoenix bearer + segments[0] === userId):
//   PUT    /<userId>/<key...>     — store an object; quota-charged (413 over quota)
//   GET    /<userId>/<key...>     — return the stored object (404 if absent)
//   GET    /<userId>/?list        — JSON array of this owner's keys (no __ prefixes),
//                                    each relative to <userId>/ (the CLI re-prepends)
//   DELETE /<userId>/<key...>     — logical delete via hidden tombstone; refunds quota
//   GET    /                      — 401 (there is no public route)
//
// Emitted as a string so it compiles into dist/** with no package.json#files
// change. `provision.ts` uploads this verbatim as an ES-module Worker with a
// BUCKET (R2) binding + a PHOENIX_ID_BASE secret.
/**
 * Render the managed sessions Worker source. Pure — bindings are wired at
 * deploy time. No `${}` interpolation in the emitted source (string
 * concatenation only) so the outer template literal never captures it.
 */
export function renderSessionsWorkerScript() {
    return `// GENERATED by agents-cli agents sessions — do not edit; edit
// src/lib/session/sync/worker-template.ts and re-provision.
export const hooks = {
  verifyPhoenixToken: defaultVerifyPhoenixToken,
};

// Free-tier limits for one owner's backup namespace. Product behavior lives in
// code; secrets are bindings, quotas are not ambient deployment knobs.
const PLAN = {
  maxBytes: 5 * 1024 * 1024 * 1024,
  maxObjects: 200000,
  maxPaths: 200000,
  maxFileBytes: 64 * 1024 * 1024,
};

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    let path;
    try {
      path = decodeURIComponent(url.pathname.replace(/^\\/+/, ''));
    } catch {
      return json({ error: 'malformed object path' }, 400);
    }

    if (!path) {
      return json({ error: 'unauthorized' }, 401);
    }

    if (request.method === 'PUT') {
      return handlePut(request, env, path);
    }
    if (request.method === 'GET' || request.method === 'HEAD') {
      return handleGet(request, env, path, url);
    }
    if (request.method === 'DELETE') {
      return handleDelete(request, env, path);
    }
    return json({ error: 'method not allowed' }, 405);
  },
};

// The reserved first-key-segment prefixes: owner-bearer only, never listed, and
// never quota-charged/refunded (the ledger and the DEK are infrastructure, not
// user payload).
function isReservedRelKey(rel) {
  const first = rel.split('/').filter(Boolean)[0] || '';
  return first === '__usage' || first === '__key';
}

async function handlePut(request, env, path) {
  const auth = await authorizeWrite(request, env);
  if (auth.error) return auth.error;

  const segments = path.split('/').filter(Boolean);
  const owner = auth.owner;
  if (!owner || segments[0] !== owner) {
    return json({ error: 'namespace mismatch', owner: owner }, 403);
  }
  const rel = segments.slice(1).join('/');
  if (!rel) return json({ error: 'missing object key' }, 400);

  const reserved = isReservedRelKey(rel);
  const isEscrow = rel === '__key/backup-dek';
  if (reserved && !isEscrow) {
    return json({ error: 'reserved object key' }, 403);
  }
  const contentType = request.headers.get('content-type') || 'application/octet-stream';

  // Read the body bounded to the per-file cap so a streaming body can never OOM
  // the Worker or slip past the quota by lying about its length.
  const read = await readBodyBounded(request, PLAN.maxFileBytes);
  if (read.oversize) {
    return json({ error: 'file too large', maxBytes: PLAN.maxFileBytes }, 413);
  }
  const body = read.bytes;

  // The escrow key is immutable: all first-use devices conditionally create it,
  // then losers read the one winner before encrypting a transcript.
  if (isEscrow) {
    const created = await env.BUCKET.put(path, body, {
      httpMetadata: { contentType: contentType },
      customMetadata: { owner: owner, uploadedAt: new Date().toISOString() },
      onlyIf: { etagDoesNotMatch: '*' },
    });
    if (created === null) return json({ error: 'already exists', key: path }, 409);
    return json({ ok: true, key: path }, 200);
  }

  // Fail closed on a plaintext payload: a managed transcript bundle is always
  // client-side-encrypted (SES-51), so an unencrypted bundle here is an
  // older/buggy client and MUST be rejected before it lands readable.
  if (!isEncryptedManagedBundle(body)) {
    return json({ error: 'payload must be an encrypted bundle' }, 422);
  }

  // Reserve a bounded historical path slot BEFORE creating its persistent lease
  // or tombstone. Live-object count is refundable; internal state is not safely
  // garbage-collectable while an unbounded-duration predecessor may resume, so
  // this separate cap prevents create/delete churn from growing R2 forever.
  const pathId = await mutationPathId(path);
  const pathReservation = await reserveMutationPath(env, owner, pathId, PLAN);
  if (pathReservation instanceof Response) return pathReservation;

  const acquired = await acquireMutationLease(env, owner, path);
  if (acquired.error) return acquired.error;
  const lease = acquired.lease;
  try {
    // PUT and DELETE share this per-key lease. The operation record is durable
    // BEFORE its usage delta, so an expired successor can reconcile an
    // interrupted request from the actual object's mutationToken metadata.
    const physicalPrior = await env.BUCKET.head(path);
    const prior = physicalPrior && !isDeletedObject(physicalPrior) ? physicalPrior : null;
    const priorBytes = prior && typeof prior.size === 'number' ? prior.size : 0;
    const operation = {
      token: lease.token,
      kind: 'put',
      path: path,
      pathId: lease.pathId,
      generation: lease.generation,
      priorEtag: physicalPrior && physicalPrior.etag ? physicalPrior.etag : null,
    };
    if (!await setLeaseOperation(env, lease, operation)) {
      return json({ error: 'mutation lease contended, retry' }, 503);
    }
    const charge = await beginUsageMutation(env, owner, {
      operation: operation,
      fileBytes: body.byteLength,
      bytes: body.byteLength - priorBytes,
      count: prior ? 0 : 1,
    }, PLAN);
    if (charge instanceof Response) {
      await setLeaseOperation(env, lease, null);
      return charge;
    }
    // Renew/fence immediately before touching the object. A successor that
    // reclaimed an expired holder prevents this request from mutating state.
    if (!await setLeaseOperation(env, lease, operation)) {
      return json({ error: 'mutation lease lost; retry' }, 409);
    }

    const putOpts = {
      httpMetadata: { contentType: contentType },
      customMetadata: { owner: owner, uploadedAt: new Date().toISOString(), mutationToken: lease.token },
      onlyIf: physicalPrior && physicalPrior.etag
        ? { etagMatches: physicalPrior.etag }
        : { etagDoesNotMatch: '*' },
    };
    let stored;
    try {
      stored = await env.BUCKET.put(path, body, putOpts);
    } catch (err) {
      const reconciled = await reconcileUsageMutation(env, owner, operation);
      if (reconciled) await setLeaseOperation(env, lease, null);
      if (!reconciled) return json({ error: 'usage reconciliation contended, retry' }, 503);
      throw err;
    }
    if (stored === null) {
      const rolledBack = await finishUsageMutation(env, owner, operation, true);
      if (rolledBack) await setLeaseOperation(env, lease, null);
      if (!rolledBack) return json({ error: 'usage reconciliation contended, retry' }, 503);
      return json({ error: 'conflict', key: path }, 409);
    }
    const committed = await finishUsageMutation(env, owner, operation, false);
    if (committed) await setLeaseOperation(env, lease, null);
    if (!committed) return json({ error: 'usage reconciliation contended, retry' }, 503);
  } finally {
    await releaseMutationLease(env, lease);
  }

  const headers = new Headers();
  headers.set('content-type', 'application/json; charset=utf-8');
  headers.set('cache-control', 'private, no-store');
  return new Response(JSON.stringify({ ok: true, key: path }), { status: 200, headers });
}

async function handleGet(request, env, path, url) {
  const auth = await authorizeRead(request, env, path);
  if (auth.error) return auth.error;

  const segments = path.split('/').filter(Boolean);
  const owner = segments[0];

  // GET /<userId>/?list — enumerate this owner's keys (never the __ reserved
  // prefixes), each relative to <userId>/ so the CLI re-prepends inside its
  // client (BYO object layout stays identical).
  if (segments.length === 1 && url.searchParams.has('list')) {
    const keys = await listOwnerKeys(env, owner);
    const headers = new Headers();
    headers.set('content-type', 'application/json; charset=utf-8');
    headers.set('cache-control', 'private, no-store');
    return new Response(JSON.stringify({ keys: keys }), { status: 200, headers });
  }

  const object = await env.BUCKET.get(path);
  if (!object || isDeletedObject(object)) return json({ error: 'not found' }, 404);

  const headers = new Headers(object.httpMetadata ?? {});
  headers.set('cache-control', 'private, no-store');
  return new Response(request.method === 'HEAD' ? null : object.body, { headers });
}

async function handleDelete(request, env, path) {
  const auth = await authorizeWrite(request, env);
  if (auth.error) return auth.error;

  const segments = path.split('/').filter(Boolean);
  const owner = auth.owner;
  if (!owner || segments[0] !== owner) {
    return json({ error: 'namespace mismatch', owner: owner }, 403);
  }
  const rel = segments.slice(1).join('/');
  if (!rel) return json({ error: 'missing object key' }, 400);

  const reserved = isReservedRelKey(rel);
  if (reserved) {
    // Reserved infrastructure records are not user-deletable: deleting the
    // immutable escrow would orphan ciphertext on the next fresh-device mint.
    return json({ error: 'reserved object key' }, 403);
  }
  const beforeLease = await env.BUCKET.head(path);
  if (!beforeLease) return json({ ok: true, key: path }, 200);
  const pathId = await mutationPathId(path);
  const pathReservation = await reserveMutationPath(env, owner, pathId, PLAN);
  if (pathReservation instanceof Response) return pathReservation;
  if (isDeletedObject(beforeLease)) return json({ ok: true, key: path }, 200);

  const acquired = await acquireMutationLease(env, owner, path);
  if (acquired.error) return acquired.error;
  const lease = acquired.lease;
  try {
    const existing = await env.BUCKET.head(path);
    if (existing && !isDeletedObject(existing)) {
      const size = typeof existing.size === 'number' ? existing.size : 0;
      const operation = {
        token: lease.token,
        kind: 'delete',
        path: path,
        pathId: lease.pathId,
        generation: lease.generation,
        priorEtag: existing.etag || null,
      };
      if (!await setLeaseOperation(env, lease, operation)) {
        return json({ error: 'mutation lease contended, retry' }, 503);
      }
      const refund = await beginUsageMutation(env, owner, {
        operation: operation, bytes: -size, count: -1,
      }, PLAN);
      if (refund instanceof Response) {
        await setLeaseOperation(env, lease, null);
        return refund;
      }
      if (!await setLeaseOperation(env, lease, operation)) {
        return json({ error: 'mutation lease lost; retry' }, 409);
      }
      try {
        const deleted = await env.BUCKET.put(path, new Uint8Array(0), {
          httpMetadata: { contentType: 'application/x-agents-session-tombstone' },
          customMetadata: {
            owner: owner,
            deleted: 'true',
            mutationToken: lease.token,
            deletedAt: new Date().toISOString(),
          },
          onlyIf: { etagMatches: existing.etag },
        });
        if (deleted === null) {
          const reconciled = await reconcileUsageMutation(env, owner, operation);
          if (reconciled) await setLeaseOperation(env, lease, null);
          if (!reconciled) return json({ error: 'usage reconciliation contended, retry' }, 503);
          return json({ error: 'conflict', key: path }, 409);
        }
      } catch (err) {
        const reconciled = await reconcileUsageMutation(env, owner, operation);
        if (reconciled) await setLeaseOperation(env, lease, null);
        if (!reconciled) return json({ error: 'usage reconciliation contended, retry' }, 503);
        throw err;
      }
      const committed = await finishUsageMutation(env, owner, operation, false);
      if (committed) await setLeaseOperation(env, lease, null);
      if (!committed) return json({ error: 'usage reconciliation contended, retry' }, 503);
    }
  } finally {
    await releaseMutationLease(env, lease);
  }

  const headers = new Headers();
  headers.set('content-type', 'application/json; charset=utf-8');
  headers.set('cache-control', 'private, no-store');
  return new Response(JSON.stringify({ ok: true, key: path }), { status: 200, headers });
}

// Recoverable, storage-backed mutex shared by managed PUT and DELETE for one
// object path. A released lease remains as a tiny internal record so release is
// an etag-guarded PUT (never an unsafe unconditional delete). An expired holder
// can be replaced, and its durable operation is reconciled before new work.
const MUTATION_LEASE_MS = 60 * 1000;

async function mutationPathId(path) {
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(path));
  return Array.from(new Uint8Array(digest)).map(function (b) { return b.toString(16).padStart(2, '0'); }).join('');
}

async function mutationLeaseKey(owner, path) {
  return '__mutation/' + owner + '/' + await mutationPathId(path);
}

async function readMutationLease(env, key) {
  const obj = await env.BUCKET.get(key);
  if (!obj) return null;
  let record = null;
  try {
    const parsed = JSON.parse(await obj.text());
    if (parsed && (parsed.v === 1 || parsed.v === 2) && typeof parsed.holder === 'string') record = parsed;
  } catch { /* malformed internal record is reclaimable */ }
  return { etag: obj.etag || null, record: record };
}

async function writeMutationLeaseCas(env, key, priorEtag, record) {
  const opts = { httpMetadata: { contentType: 'application/json' } };
  opts.onlyIf = priorEtag ? { etagMatches: priorEtag } : { etagDoesNotMatch: '*' };
  const stored = await env.BUCKET.put(key, JSON.stringify(record), opts);
  return stored !== null;
}

async function acquireMutationLease(env, owner, path) {
  const key = await mutationLeaseKey(owner, path);
  const pathId = key.slice(key.lastIndexOf('/') + 1);
  for (let attempt = 0; attempt < 6; attempt++) {
    const state = await readMutationLease(env, key);
    const now = Date.now();
    if (state && state.record && state.record.expiresAt > now) {
      return { error: json({ error: 'object mutation already in progress; retry' }, 409) };
    }
    const token = crypto.randomUUID();
    const generation = state && state.record && Number.isFinite(state.record.generation)
      ? state.record.generation + 1
      : 1;
    // Preserve an expired holder's operation in the replacement record. If this
    // Worker dies during recovery, the next successor still sees the same work.
    const record = {
      v: 2,
      generation: generation,
      holder: token,
      expiresAt: now + MUTATION_LEASE_MS,
      operation: state && state.record ? state.record.operation || null : null,
    };
    if (!await writeMutationLeaseCas(env, key, state && state.etag, record)) continue;
    const lease = { key: key, token: token, pathId: pathId, generation: generation };
    if (record.operation) {
      // Before settling an expired predecessor, invalidate the exact object etag
      // it was authorized to mutate. A predecessor that later resumes can no
      // longer PUT or DELETE the successor's object.
      const recovered = await fenceAndReconcileStaleMutation(env, owner, record.operation, token);
      if (!recovered || !await setLeaseOperation(env, lease, null)) {
        await releaseMutationLease(env, lease);
        return { error: json({ error: 'stale mutation reconciliation contended, retry' }, 503) };
      }
    }
    return { lease: lease };
  }
  return { error: json({ error: 'mutation lease contended, retry' }, 503) };
}

async function setLeaseOperation(env, lease, operation) {
  for (let attempt = 0; attempt < 6; attempt++) {
    const state = await readMutationLease(env, lease.key);
    if (!state || !state.record || state.record.holder !== lease.token) return false;
    const next = {
      v: 2,
      generation: lease.generation,
      holder: lease.token,
      expiresAt: Date.now() + MUTATION_LEASE_MS,
      operation: operation,
    };
    if (await writeMutationLeaseCas(env, lease.key, state.etag, next)) return true;
  }
  return false;
}

async function releaseMutationLease(env, lease) {
  for (let attempt = 0; attempt < 6; attempt++) {
    const state = await readMutationLease(env, lease.key);
    if (!state || !state.record || state.record.holder !== lease.token) return false;
    // Keep any unsettled operation recoverable, but make the lease immediately
    // acquirable. A normal completed request cleared operation first.
    const next = {
      v: 2,
      generation: lease.generation,
      holder: '',
      expiresAt: 0,
      operation: state.record.operation || null,
    };
    if (await writeMutationLeaseCas(env, lease.key, state.etag, next)) return true;
  }
  return false;
}

async function reconcileUsageMutation(env, owner, operation) {
  if (!validMutationOperation(operation)) return true;
  const current = await env.BUCKET.head(operation.path);
  return finishUsageMutation(env, owner, operation, !mutationApplied(current, operation));
}

function validMutationOperation(operation) {
  return !!operation
    && typeof operation.token === 'string'
    && typeof operation.path === 'string'
    && typeof operation.pathId === 'string'
    && Number.isFinite(operation.generation);
}

function mutationApplied(object, operation) {
  const tokenMatches = !!object && !!object.customMetadata
    && object.customMetadata.mutationToken === operation.token;
  return operation.kind === 'delete'
    ? tokenMatches && isDeletedObject(object)
    : tokenMatches && !isDeletedObject(object);
}

// Reclaim an expired predecessor safely. Its object mutation was conditional on
// priorEtag, so we first change/create that physical object under the same etag
// condition. Exactly one side wins: if the predecessor already won, its
// mutationToken is visible and its quota delta commits; if this fence wins, the
// predecessor's later conditional mutation must fail and its delta rolls back.
async function fenceAndReconcileStaleMutation(env, owner, operation, successorToken) {
  if (!validMutationOperation(operation)) return true;
  for (let attempt = 0; attempt < 6; attempt++) {
    const current = await env.BUCKET.get(operation.path);
    if (mutationApplied(current, operation)) {
      return finishUsageMutation(env, owner, operation, false);
    }

    const priorEtag = typeof operation.priorEtag === 'string' ? operation.priorEtag : null;
    if (priorEtag && current && current.etag === priorEtag) {
      const bytes = new Uint8Array(await current.arrayBuffer());
      const metadata = Object.assign({}, current.customMetadata || {}, { fencedBy: successorToken });
      const fenced = await env.BUCKET.put(operation.path, bytes, {
        httpMetadata: current.httpMetadata || {},
        customMetadata: metadata,
        onlyIf: { etagMatches: current.etag },
      });
      if (fenced === null) continue;
    } else if (!priorEtag && !current) {
      const fenced = await env.BUCKET.put(operation.path, new Uint8Array(0), {
        httpMetadata: { contentType: 'application/x-agents-session-tombstone' },
        customMetadata: { owner: owner, deleted: 'true', fencedBy: successorToken },
        onlyIf: { etagDoesNotMatch: '*' },
      });
      if (fenced === null) continue;
    }
    // The prior etag is now absent, changed, or fenced by us. A late predecessor
    // cannot pass its object conditional. Persist its terminal generation in the
    // SAME CAS ledger that stores pending deltas before rolling back.
    return finishUsageMutation(env, owner, operation, true);
  }
  return false;
}

// Enumerate every stored key under <owner>/, EXCLUDING the reserved __ prefixes,
// returning each stripped of the <owner>/ prefix. Paginates R2's cursor so a
// large backup set lists completely.
async function listOwnerKeys(env, owner) {
  const base = owner + '/';
  const out = [];
  let cursor = undefined;
  do {
    const listOpts = cursor
      ? { prefix: base, cursor: cursor, include: ['customMetadata'] }
      : { prefix: base, include: ['customMetadata'] };
    const listed = await env.BUCKET.list(listOpts);
    const objects = (listed && listed.objects) || [];
    for (const o of objects) {
      const key = o.key || '';
      if (!key.startsWith(base)) continue;
      const rel = key.slice(base.length);
      if (!rel || isReservedRelKey(rel) || isDeletedObject(o)) continue;
      out.push(rel);
    }
    cursor = listed && listed.truncated ? listed.cursor : undefined;
  } while (cursor);
  return out.sort();
}

function isDeletedObject(object) {
  return !!object && !!object.customMetadata && object.customMetadata.deleted === 'true';
}

// --- Per-user usage ledger (R2 conditional-put CAS), mirrored from agents-share.
// A single object at __usage/<owner> holds
// { bytes, count, pathCount, pending, settled } for one user. R2 has no atomic
// increment, so mutations use read → mutate →
// conditional-put (onlyIf.etagMatches). The __usage key is reserved:
// owner-bearer only, never listed.
function usageKey(owner) { return '__usage/' + owner; }

function usageNumber(v, dflt) {
  const n = typeof v === 'number' ? v : parseFloat(v);
  return Number.isFinite(n) && n >= 0 ? n : dflt;
}

async function readUsage(env, owner) {
  const obj = await env.BUCKET.get(usageKey(owner));
  if (!obj) return { etag: null, usage: { bytes: 0, count: 0, pathCount: 0, pending: {}, settled: {} } };
  let usage = { bytes: 0, count: 0, pathCount: 0, pending: {}, settled: {} };
  try {
    const raw = typeof obj.text === 'function' ? await obj.text() : '';
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      const settled = parsed.settled && typeof parsed.settled === 'object' ? parsed.settled : {};
      usage = {
        bytes: usageNumber(parsed.bytes, 0),
        count: usageNumber(parsed.count, 0),
        pathCount: usageNumber(parsed.pathCount, Object.keys(settled).length),
        pending: parsed.pending && typeof parsed.pending === 'object' ? parsed.pending : {},
        settled: settled,
      };
    }
  } catch (e) {
    // Malformed ledger — treat as fresh zero but KEEP the etag so the next CAS
    // put overwrites the corrupt object rather than looping against it forever.
  }
  return { etag: obj.etag || null, usage: usage };
}

async function writeUsageCas(env, owner, prevEtag, usage) {
  if (usage.pending && Object.keys(usage.pending).length === 0) delete usage.pending;
  const body = JSON.stringify(usage);
  const opts = { httpMetadata: { contentType: 'application/json' } };
  if (prevEtag) {
    opts.onlyIf = { etagMatches: prevEtag };
    const res = await env.BUCKET.put(usageKey(owner), body, opts);
    return res !== null;
  }
  opts.onlyIf = { etagDoesNotMatch: '*' };
  const res = await env.BUCKET.put(usageKey(owner), body, opts);
  return res !== null;
}

// CAS retry loop. fn(usage) returns { reject: Response } to fail loud without
// committing, or { commit: usage } to persist. On CAS contention it re-reads and
// retries; exhausting the retries fails loud with 503.
async function withUsage(env, owner, fn) {
  for (let attempt = 0; attempt < 6; attempt++) {
    const state = await readUsage(env, owner);
    const outcome = fn(state.usage);
    if (outcome.reject) return outcome.reject;
    if (outcome.skip) return null;
    const ok = await writeUsageCas(env, owner, state.etag, outcome.commit);
    if (ok) return null;
  }
  return json({ error: 'usage ledger contended, retry' }, 503);
}

// Permanently reserve one logical object-path hash in the quota ledger before
// any persistent per-path state is created. DELETE refunds live bytes/count but
// not this historical slot: tombstones, mutation leases, and settled generations
// cannot be time-GCed safely because HTTP invocations have unbounded wall time.
// The path cap therefore bounds every internal structure under adversarial churn.
async function reserveMutationPath(env, owner, pathId, limits) {
  return withUsage(env, owner, function (usage) {
    usage.settled = usage.settled && typeof usage.settled === 'object' ? usage.settled : {};
    const hasPath = Object.prototype.hasOwnProperty.call(usage.settled, pathId);
    const derivedCount = Object.keys(usage.settled).length;
    const pathCount = usageNumber(usage.pathCount, derivedCount);
    if (hasPath) {
      if (usage.pathCount === pathCount) return { skip: true };
      usage.pathCount = pathCount;
      return { commit: usage };
    }
    if (pathCount + 1 > limits.maxPaths) {
      return { reject: json({ error: 'historical path limit reached', maxPaths: limits.maxPaths }, 413) };
    }
    usage.pathCount = pathCount + 1;
    usage.settled[pathId] = 0;
    return { commit: usage };
  });
}

// Apply one idempotent usage delta and retain it under pending[token] until the
// corresponding object mutation commits or rolls back. A stale lease successor
// can therefore finish an interrupted two-object transaction without guessing
// whether the CAS ledger mutation landed.
async function beginUsageMutation(env, owner, params, limits) {
  if (typeof params.fileBytes === 'number' && params.fileBytes > limits.maxFileBytes) {
    return json({ error: 'file too large', maxBytes: limits.maxFileBytes, gotBytes: params.fileBytes }, 413);
  }
  return withUsage(env, owner, function (usage) {
    const operation = params.operation;
    usage.pending = usage.pending && typeof usage.pending === 'object' ? usage.pending : {};
    usage.settled = usage.settled && typeof usage.settled === 'object' ? usage.settled : {};
    const settledGeneration = usageNumber(usage.settled[operation.pathId], 0);
    if (settledGeneration >= operation.generation) {
      return { reject: json({ error: 'mutation superseded; retry' }, 409) };
    }
    if (usage.pending[operation.token]) return { commit: usage };
    const requestedCount = typeof params.count === 'number' ? params.count : 0;
    const requestedBytes = typeof params.bytes === 'number' ? params.bytes : 0;
    if (usage.count + requestedCount > limits.maxObjects) {
      return { reject: json({ error: 'artifact limit reached', maxObjects: limits.maxObjects }, 413) };
    }
    if (usage.bytes + requestedBytes > limits.maxBytes) {
      return { reject: json({ error: 'storage limit reached', maxBytes: limits.maxBytes, usedBytes: usage.bytes }, 413) };
    }
    const nextBytes = Math.max(0, usage.bytes + requestedBytes);
    const nextCount = Math.max(0, usage.count + requestedCount);
    usage.pending[operation.token] = {
      bytes: nextBytes - usage.bytes,
      count: nextCount - usage.count,
      pathId: operation.pathId,
      generation: operation.generation,
    };
    usage.bytes = nextBytes;
    usage.count = nextCount;
    return { commit: usage };
  });
}

// Settle a pending delta and persist a bounded terminal generation for this
// object path in the SAME CAS ledger. A late predecessor that read an older
// ledger etag cannot add its delta after recovery: its CAS loses, then begin sees
// settled[pathId] >= generation and refuses it. One entry per logical path keeps
// this tombstone bounded independently of overwrite count.
async function finishUsageMutation(env, owner, operation, rollback) {
  for (let attempt = 0; attempt < 6; attempt++) {
    const state = await readUsage(env, owner);
    state.usage.pending = state.usage.pending && typeof state.usage.pending === 'object'
      ? state.usage.pending
      : {};
    state.usage.settled = state.usage.settled && typeof state.usage.settled === 'object'
      ? state.usage.settled
      : {};
    const pending = state.usage.pending[operation.token];
    const priorSettled = usageNumber(state.usage.settled[operation.pathId], 0);
    if (!pending && priorSettled >= operation.generation) return true;
    if (rollback && pending) {
      const bytes = typeof pending.bytes === 'number' && Number.isFinite(pending.bytes) ? pending.bytes : 0;
      const count = typeof pending.count === 'number' && Number.isFinite(pending.count) ? pending.count : 0;
      state.usage.bytes = Math.max(0, state.usage.bytes - bytes);
      state.usage.count = Math.max(0, state.usage.count - count);
    }
    delete state.usage.pending[operation.token];
    state.usage.settled[operation.pathId] = Math.max(priorSettled, operation.generation);
    if (await writeUsageCas(env, owner, state.etag, state.usage)) return true;
  }
  return false;
}

// Read a request body fully into memory, BOUNDED: abort the moment it exceeds
// maxBytes. Returns { oversize: true, size } once the cap is passed, else
// { bytes, size } with the exact bytes.
async function readBodyBounded(request, maxBytes) {
  if (!request.body || typeof request.body.getReader !== 'function') {
    const buf = await request.arrayBuffer();
    const bytes = new Uint8Array(buf);
    if (bytes.byteLength > maxBytes) return { oversize: true, size: bytes.byteLength };
    return { bytes: bytes, size: bytes.byteLength };
  }
  const reader = request.body.getReader();
  const chunks = [];
  let size = 0;
  while (true) {
    const step = await reader.read();
    if (step.done) break;
    const chunk = step.value;
    size += chunk.byteLength;
    if (size > maxBytes) {
      try { await reader.cancel(); } catch (e) { /* best-effort */ }
      return { oversize: true, size: size };
    }
    chunks.push(chunk);
  }
  const out = new Uint8Array(size);
  let offset = 0;
  for (let i = 0; i < chunks.length; i++) { out.set(chunks[i], offset); offset += chunks[i].byteLength; }
  return { bytes: out, size: size };
}

// Require a Phoenix bearer that owns the path prefix. No public GET, ever.
// This managed store is Phoenix-only: there is no static-token principal. The
// zero-knowledge BYO path talks to the operator's OWN R2 bucket directly through
// the CLI's R2Client and never reaches this Worker, so a WRITE_TOKEN here would
// be a Phoenix-and-quota bypass with no legitimate caller — it is deliberately
// absent (PHNX-3726 fix-forward).
async function authorizeRead(request, env, path) {
  const claims = await hooks.verifyPhoenixToken(request, env);
  if (!claims) {
    return { error: json({ error: 'unauthorized' }, 401) };
  }
  const ownerId = path.split('/').filter(Boolean)[0] || '';
  if (!ownerId || ownerId !== claims.userId) {
    return { error: json({ error: 'forbidden' }, 403) };
  }
  return { claims: claims, owner: claims.userId };
}

// The single write principal is a Phoenix bearer whose verified userId owns the
// path prefix. 401 when it does not authenticate.
async function authorizeWrite(request, env) {
  const claims = await hooks.verifyPhoenixToken(request, env);
  if (claims && typeof claims.userId === 'string' && claims.userId) {
    return { owner: claims.userId };
  }
  return { error: json({ error: 'unauthorized' }, 401) };
}

// A stored managed body MUST be an ENCRYPTED bundle — never plaintext (SES-51).
// The upload wire is NDJSON: a header line then one record line per transcript
// (serializeBundle). This Worker independently re-checks what the CLI already
// fails-closed on before upload: the header claims encryption AND every record
// body is a real AES-256-GCM transcript envelope (v/alg/iv/ct/tag), so an
// older/buggy client's plaintext PUT is rejected 422 rather than landing readable
// in the bucket. Reserved __key / __usage records are exempt (not user payload).
function isTranscriptEnvelopeString(body) {
  if (typeof body !== 'string') return false;
  const trimmed = body.trimStart();
  if (!trimmed.startsWith('{')) return false;
  let obj;
  try {
    obj = JSON.parse(body);
  } catch {
    return false;
  }
  return !!obj
    && typeof obj === 'object'
    && obj.v === 1
    && obj.alg === 'aes-256-gcm'
    && base64ByteLength(obj.iv) === 12
    && base64ByteLength(obj.tag) === 16
    && base64ByteLength(obj.ct) >= 0;
}

function isEncryptedManagedBundle(bytes) {
  let text;
  try {
    text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
  } catch {
    return false;
  }
  const lines = text.split(/\\r?\\n/).filter(function (l) { return l.trim().length > 0; });
  if (lines.length < 2) return false; // a header line + at least one record line
  let header;
  try {
    header = JSON.parse(lines[0]);
  } catch {
    return false;
  }
  if (!header
    || header.kind !== 'agents-session-bundle'
    || header.version !== 1
    || header.encrypted !== true
    || header.count !== lines.length - 1
    || header.count < 1) return false;
  for (let i = 1; i < lines.length; i++) {
    let rec;
    try {
      rec = JSON.parse(lines[i]);
    } catch {
      return false;
    }
    if (!rec || rec.encrypted !== true || !isTranscriptEnvelopeString(rec.body)) return false;
  }
  return true;
}

function base64ByteLength(value) {
  if (typeof value !== 'string') return -1;
  try { return atob(value).length; } catch { return -1; }
}

async function defaultVerifyPhoenixToken(request, env) {
  const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
  if (!presented) return null;
  const base = typeof env.PHOENIX_ID_BASE === 'string' ? env.PHOENIX_ID_BASE.replace(/\\/+$/, '') : '';
  if (!base) return null;
  let res;
  try {
    res = await fetch(base + '/api/v1/auth/me', {
      method: 'GET',
      headers: { Authorization: 'Bearer ' + presented },
    });
  } catch {
    return null;
  }
  if (!res.ok) return null;
  let body;
  try {
    body = await res.json();
  } catch {
    return null;
  }
  if (!body || typeof body.userId !== 'string' || !body.userId) return null;
  return { userId: body.userId, email: typeof body.email === 'string' ? body.email : '' };
}

function json(body, status = 200) {
  return new Response(JSON.stringify(body), {
    status,
    headers: {
      'content-type': 'application/json; charset=utf-8',
      'cache-control': 'private, no-store',
    },
  });
}
`;
}
