// Generated by bynkc — do not edit by hand.
// bynk runtime — shared by every emitted module.

export type Result<T, E> =
  | { readonly tag: "Ok"; readonly value: T }
  | { readonly tag: "Err"; readonly error: E };

export const Ok = <T>(value: T): Result<T, never> => ({ tag: "Ok", value });
export const Err = <E>(error: E): Result<never, E> => ({ tag: "Err", error });

export type Option<T> =
  | { readonly tag: "Some"; readonly value: T }
  | { readonly tag: "None" };

export const Some = <T>(value: T): Option<T> => ({ tag: "Some", value });
export const None: Option<never> = { tag: "None" };

// v0.110 (ADR 0142): the `Bytes` primitive runtime. A `Bytes` erases to a
// `Uint8Array` (an immutable finite octet sequence at the Bynk level), so
// unlike the number-erased base types its equality is by content, not host
// `===`, and its wire form is a base64 string. These helpers back the kernel
// (`==`, `toBase64`, `decodeUtf8`) and the JSON codec (`fromBase64`).

// Content equality — the byte-for-byte compare the emitter lowers `==` to. The
// `===` fast path catches the identity case; otherwise length then each octet.
export function __bynkBytesEqual(a: Uint8Array, b: Uint8Array): boolean {
  if (a === b) return true;
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

// Total encode: the standard base64 of the octet sequence. Every byte is
// 0..255, so the Latin-1 string handed to `btoa` never overflows.
export function __bynkBytesToBase64(bytes: Uint8Array): string {
  let bin = "";
  for (let i = 0; i < bytes.length; i++) {
    bin += String.fromCharCode(bytes[i]);
  }
  return btoa(bin);
}

// Partial decode: `Some(bytes)` for a valid standard-base64 string, `None`
// otherwise. The alphabet/padding are validated up front so a malformed string
// is a clean `None`, not a silently truncated buffer (some `atob`
// implementations are lenient). The empty string decodes to empty bytes — the
// round-trip of `Bytes.empty`.
export function __bynkBytesFromBase64(s: string): Option<Uint8Array> {
  if (s.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(s)) {
    return None;
  }
  let bin: string;
  try {
    bin = atob(s);
  } catch {
    return None;
  }
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) {
    out[i] = bin.charCodeAt(i);
  }
  return Some(out);
}

// Partial decode to text: `Some(string)` when the octets are valid UTF-8,
// `None` otherwise. `fatal: true` makes an invalid sequence throw rather than
// emit replacement characters, so the partiality is real and surfaced.
export function __bynkBytesDecodeUtf8(bytes: Uint8Array): Option<string> {
  try {
    return Some(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
  } catch {
    return None;
  }
}

export interface ValidationError {
  readonly field: string;
  readonly message: string;
  readonly value: unknown;
}

// v0.22b: the JSON-decode error (ADR 0047) — BoundaryError's information
// (the discriminating kind and the tracked field path) flattened into a
// uniform, Bynk-inspectable record. `kind` is "Malformed" for a JSON.parse
// failure, else the BoundaryError kind ("StructuralMismatch",
// "RefinementViolation").
export interface JsonError {
  readonly kind: string;
  readonly path: string;
  readonly message: string;
}

// v0.80 (§14): an agent invariant violated at the commit boundary. A dedicated
// internal fault — distinct from BoundaryError (the cross-Worker call/refinement
// layer) — thrown inside the generated `commitState` *before* the proposed state
// is persisted, so the offending commit never lands. It rides the existing
// uncaught-fault channel: the caller observes a fault, not an outcome (ADR 0107).
export interface InvariantViolation {
  readonly kind: "InvariantViolation";
  readonly agent: string;
  readonly invariant: string;
}

export function invariantViolation(agent: string, invariant: string): Error {
  const e = new Error(`InvariantViolation: ${agent}.${invariant}`);
  (e as { invariantViolation?: InvariantViolation }).invariantViolation = {
    kind: "InvariantViolation",
    agent,
    invariant,
  };
  return e;
}

export interface DurableObjectStorage {
  get<T>(key: string): Promise<T | undefined>;
  put(key: string, value: unknown): Promise<void>;
  delete(key: string): Promise<boolean>;
  list<T>(options?: { prefix?: string }): Promise<Map<string, T>>;
}

export interface DurableObjectState {
  readonly storage: DurableObjectStorage;
  readonly id: { readonly name: string };
}

export class InMemoryStorage implements DurableObjectStorage {
  private data = new Map<string, unknown>();

  async get<T>(key: string): Promise<T | undefined> {
    return this.data.get(key) as T | undefined;
  }

  async put(key: string, value: unknown): Promise<void> {
    this.data.set(key, value);
  }

  async delete(key: string): Promise<boolean> {
    return this.data.delete(key);
  }

  async list<T>(options?: { prefix?: string }): Promise<Map<string, T>> {
    const prefix = options?.prefix ?? "";
    const out = new Map<string, T>();
    for (const [k, v] of this.data) {
      if (k.startsWith(prefix)) out.set(k, v as T);
    }
    return out;
  }
}

export function makeTestState(name: string): DurableObjectState {
  return {
    storage: new InMemoryStorage(),
    id: { name },
  };
}

export interface KVNamespace {
  get(key: string): Promise<string | null>;
  put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;
  delete(key: string): Promise<void>;
  // v0.23: the page shape WorkersKv's drain consumes (0050).
  list(options?: { prefix?: string; cursor?: string }): Promise<{
    keys: { name: string }[];
    list_complete: boolean;
    cursor?: string;
  }>;
}

// v0.8: cross-Worker boundary protocol — JSON wire format and error types.

export type JsonValue =
  | null
  | boolean
  | number
  | string
  | JsonValue[]
  | { [k: string]: JsonValue };

export type BoundaryError =
  | { readonly kind: "MalformedJson"; readonly details: string }
  | {
      readonly kind: "StructuralMismatch";
      readonly path: string;
      readonly expected: string;
      readonly actual: string;
    }
  | {
      readonly kind: "RefinementViolation";
      readonly path: string;
      readonly violation: ValidationError;
    }
  | { readonly kind: "Transport"; readonly status: number; readonly details: string };

export function boundaryError(error: BoundaryError): Error {
  const e = new Error(`BoundaryError: ${error.kind}`);
  (e as any).boundaryError = error;
  return e;
}

// v0.96 (ADR 0124): an agent's persisted state failed validation on rehydration —
// a refined field, key, or entry no longer satisfies the current type definition
// (schema corruption, or a refinement that tightened across a deploy, orphaning
// previously-valid data). The load-time twin of InvariantViolation: a dedicated
// internal fault, NOT a caller-facing BoundaryError, because the supplier is
// trusted past-self, not the untrusted caller (Q6). It reuses the boundary
// validator's *detection* (the BoundaryError detail) but disposes of it as a
// fault. Logged with the agent type and field path only — never the key or the
// offending value (ADR 0107 logging discipline).
export interface RehydrationViolation {
  readonly kind: "RehydrationViolation";
  readonly agent: string;
  readonly path: string;
  readonly detail: BoundaryError;
}

export function rehydrationViolation(agent: string, detail: BoundaryError): Error {
  const path = "path" in detail ? detail.path : "<root>";
  const e = new Error(`RehydrationViolation: ${agent} ${detail.kind} at ${path}`);
  (e as { rehydrationViolation?: RehydrationViolation }).rehydrationViolation = {
    kind: "RehydrationViolation",
    agent,
    path,
    detail,
  };
  return e;
}

export interface ServiceBinding {
  fetch(request: Request): Promise<Response>;
}

export async function callService<T, E>(
  binding: ServiceBinding,
  servicePath: string,
  argsJson: JsonValue,
  deserialiseResult: (json: JsonValue) => Result<Result<T, E>, BoundaryError>,
  // v0.54: the calling context's qualified name, stamped beside the args so the
  // callee's `by c: Caller` handler can present a live `CallerId` (Q7). A
  // compile-time constant; the args body itself is unchanged. The `Internal`
  // channel trusts the binding, so this is identity, not authentication.
  callerContext: string = "",
): Promise<Result<T, E>> {
  const request = new Request(`http://internal/_bynk/call/${servicePath}`, {
    method: "POST",
    headers: { "content-type": "application/json", "X-Bynk-Caller": callerContext },
    body: JSON.stringify(argsJson),
  });
  const response = await binding.fetch(request);
  if (!response.ok) {
    throw boundaryError({
      kind: "Transport",
      status: response.status,
      details: await response.text(),
    });
  }
  let responseJson: JsonValue;
  try {
    responseJson = (await response.json()) as JsonValue;
  } catch (e) {
    throw boundaryError({ kind: "MalformedJson", details: String(e) });
  }
  const result = deserialiseResult(responseJson);
  if (result.tag === "Err") {
    throw boundaryError(result.error);
  }
  return result.value;
}

// v0.9: HttpResult — the built-in HTTP-result sum.

export type HttpResult<T> =
  // 2xx success — carries the serialised value.
  | { readonly tag: "Ok"; readonly value: T }
  // 2xx streamed body — SSE-framed (real-time track slice 1).
  | { readonly tag: "Streaming"; readonly stream: AsyncIterable<string> }
  // 2xx raw body — author-owned bytes with an explicit content-type (v0.111).
  | { readonly tag: "Raw"; readonly body: Uint8Array; readonly contentType: string }
  | { readonly tag: "Created"; readonly value: T }
  | { readonly tag: "Accepted"; readonly value: T }
  | { readonly tag: "NoContent" }
  // 3xx redirection — carries the target URL, emitted as a Location header.
  | { readonly tag: "MovedPermanently"; readonly location: string }
  | { readonly tag: "Found"; readonly location: string }
  | { readonly tag: "SeeOther"; readonly location: string }
  | { readonly tag: "TemporaryRedirect"; readonly location: string }
  | { readonly tag: "PermanentRedirect"; readonly location: string }
  // 4xx client error.
  | { readonly tag: "BadRequest"; readonly message: string }
  | { readonly tag: "Unauthorized" }
  | { readonly tag: "Forbidden" }
  | { readonly tag: "NotFound" }
  | { readonly tag: "MethodNotAllowed" }
  | { readonly tag: "NotAcceptable" }
  | { readonly tag: "RequestTimeout" }
  | { readonly tag: "Conflict"; readonly message: string }
  | { readonly tag: "Gone" }
  | { readonly tag: "LengthRequired" }
  | { readonly tag: "PayloadTooLarge"; readonly message: string }
  | { readonly tag: "UnsupportedMediaType"; readonly message: string }
  | { readonly tag: "UnprocessableEntity"; readonly message: string }
  | { readonly tag: "TooManyRequests"; readonly message: string }
  | { readonly tag: "UnavailableForLegalReasons"; readonly message: string }
  // 5xx server error.
  | { readonly tag: "ServerError"; readonly message: string }
  | { readonly tag: "NotImplemented"; readonly message: string }
  | { readonly tag: "BadGateway"; readonly message: string }
  | { readonly tag: "ServiceUnavailable"; readonly message: string }
  | { readonly tag: "GatewayTimeout"; readonly message: string };

export const HttpResult = {
  // 2xx success.
  Ok: <T>(value: T): HttpResult<T> => ({ tag: "Ok", value }),
  // 2xx streamed body — the argument is a stream of SSE event payloads.
  Streaming: (stream: AsyncIterable<string>): HttpResult<never> => ({ tag: "Streaming", stream }),
  // 2xx raw body — the arguments are the octets and their content-type.
  Raw: (body: Uint8Array, contentType: string): HttpResult<never> => ({ tag: "Raw", body, contentType }),
  Created: <T>(value: T): HttpResult<T> => ({ tag: "Created", value }),
  Accepted: <T>(value: T): HttpResult<T> => ({ tag: "Accepted", value }),
  NoContent: { tag: "NoContent" } as HttpResult<never>,
  // 3xx redirection — the argument is the target URL (Location header).
  MovedPermanently: (location: string): HttpResult<never> => ({ tag: "MovedPermanently", location }),
  Found: (location: string): HttpResult<never> => ({ tag: "Found", location }),
  SeeOther: (location: string): HttpResult<never> => ({ tag: "SeeOther", location }),
  TemporaryRedirect: (location: string): HttpResult<never> => ({ tag: "TemporaryRedirect", location }),
  PermanentRedirect: (location: string): HttpResult<never> => ({ tag: "PermanentRedirect", location }),
  // 4xx client error.
  BadRequest: (message: string): HttpResult<never> => ({ tag: "BadRequest", message }),
  Unauthorized: { tag: "Unauthorized" } as HttpResult<never>,
  Forbidden: { tag: "Forbidden" } as HttpResult<never>,
  NotFound: { tag: "NotFound" } as HttpResult<never>,
  MethodNotAllowed: { tag: "MethodNotAllowed" } as HttpResult<never>,
  NotAcceptable: { tag: "NotAcceptable" } as HttpResult<never>,
  RequestTimeout: { tag: "RequestTimeout" } as HttpResult<never>,
  Conflict: (message: string): HttpResult<never> => ({ tag: "Conflict", message }),
  Gone: { tag: "Gone" } as HttpResult<never>,
  LengthRequired: { tag: "LengthRequired" } as HttpResult<never>,
  PayloadTooLarge: (message: string): HttpResult<never> => ({ tag: "PayloadTooLarge", message }),
  UnsupportedMediaType: (message: string): HttpResult<never> => ({
    tag: "UnsupportedMediaType",
    message,
  }),
  UnprocessableEntity: (message: string): HttpResult<never> => ({
    tag: "UnprocessableEntity",
    message,
  }),
  TooManyRequests: (message: string): HttpResult<never> => ({ tag: "TooManyRequests", message }),
  UnavailableForLegalReasons: (message: string): HttpResult<never> => ({
    tag: "UnavailableForLegalReasons",
    message,
  }),
  // 5xx server error.
  ServerError: (message: string): HttpResult<never> => ({ tag: "ServerError", message }),
  NotImplemented: (message: string): HttpResult<never> => ({ tag: "NotImplemented", message }),
  BadGateway: (message: string): HttpResult<never> => ({ tag: "BadGateway", message }),
  ServiceUnavailable: (message: string): HttpResult<never> => ({ tag: "ServiceUnavailable", message }),
  GatewayTimeout: (message: string): HttpResult<never> => ({ tag: "GatewayTimeout", message }),
};

// Match a path pattern (e.g., "/orders/:id") against a request path.
// Returns the captured parameter map, or null on no match.
export function matchPath(
  pattern: string,
  path: string,
): { params: Record<string, string> } | null {
  const patternSegments = pattern.split("/").filter(Boolean);
  const pathSegments = path.split("/").filter(Boolean);
  if (patternSegments.length !== pathSegments.length) return null;
  const params: Record<string, string> = {};
  for (let i = 0; i < patternSegments.length; i++) {
    const p = patternSegments[i];
    if (p.startsWith(":")) {
      params[p.slice(1)] = decodeURIComponent(pathSegments[i]);
    } else if (p !== pathSegments[i]) {
      return null;
    }
  }
  return { params };
}

// The HTTP status code each HttpResult variant maps to. Kept in sync with
// HTTP_VARIANTS in bynk-syntax/src/ast.rs (the compiler-side source of truth).
const HTTP_STATUS: Record<HttpResult<unknown>["tag"], number> = {
  Ok: 200,
  Streaming: 200,
  Raw: 200,
  Created: 201,
  Accepted: 202,
  NoContent: 204,
  MovedPermanently: 301,
  Found: 302,
  SeeOther: 303,
  TemporaryRedirect: 307,
  PermanentRedirect: 308,
  BadRequest: 400,
  Unauthorized: 401,
  Forbidden: 403,
  NotFound: 404,
  MethodNotAllowed: 405,
  NotAcceptable: 406,
  RequestTimeout: 408,
  Conflict: 409,
  Gone: 410,
  LengthRequired: 411,
  PayloadTooLarge: 413,
  UnsupportedMediaType: 415,
  UnprocessableEntity: 422,
  TooManyRequests: 429,
  UnavailableForLegalReasons: 451,
  ServerError: 500,
  NotImplemented: 501,
  BadGateway: 502,
  ServiceUnavailable: 503,
  GatewayTimeout: 504,
};

// Serialise an HttpResult<T> to a Response. The variant determines the HTTP
// status code; success variants carry the value as JSON, redirects emit a
// Location header, error variants carry an `{ error }` body, and the remaining
// statuses are bodyless.
// Frame a stream of event payloads as an SSE (`text/event-stream`) Response.
// Each stream element is one SSE event; a multi-line element becomes multiple
// `data:` lines, terminated by a blank line. The body is a ReadableStream, so
// this is a Web standard that runs unchanged on Workers and Node.
function sseResponse(stream: AsyncIterable<string>): Response {
  const encoder = new TextEncoder();
  // v0.139 (ADR 0162): a `pull` source with `highWaterMark: 0` makes the body
  // lazy — the source stream is advanced only when a reader pulls, never
  // eagerly at construction. So a `Streaming` `GET` answered as `HEAD` (whose
  // `Response` body is discarded unread) never advances the source at all, and a
  // real `GET` streams under the consumer's backpressure rather than buffering
  // the whole stream up front. `cancel` returns the iterator so an abandoned
  // read releases the source promptly.
  const iterator = stream[Symbol.asyncIterator]();
  const body = new ReadableStream<Uint8Array>(
    {
      async pull(controller) {
        const { value, done } = await iterator.next();
        if (done) {
          controller.close();
          return;
        }
        for (const line of value.split("\n")) {
          controller.enqueue(encoder.encode(`data: ${line}\n`));
        }
        controller.enqueue(encoder.encode("\n"));
      },
      async cancel(reason) {
        await iterator.return?.(reason);
      },
    },
    { highWaterMark: 0 },
  );
  return new Response(body, {
    status: 200,
    headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
  });
}

export function httpResultToResponse<T>(
  result: HttpResult<T>,
  serialiseValue: (v: T) => JsonValue,
  opts?: { readonly weakEtag?: boolean },
): Response {
  const status = HTTP_STATUS[result.tag];
  switch (result.tag) {
    // 2xx with a body — the serialised value as JSON.
    case "Ok":
    case "Created":
    case "Accepted": {
      const body = JSON.stringify(serialiseValue(result.value));
      const headers: Record<string, string> = { "content-type": "application/json" };
      // v0.140 (ADR 0163): an eligible `GET` `Ok` carries a weak validator over its
      // serialised body, so a conditional re-fetch can be answered `304`. Only `Ok`
      // (the JSON success read) is eligible (DECISION B), and only when the caller
      // opts in — the `GET` dispatch site passes `weakEtag`, so a POST/PUT `Ok` (or
      // any other method) stays byte-for-byte unchanged.
      if (opts?.weakEtag && result.tag === "Ok") {
        headers["etag"] = weakETag(body);
      }
      return new Response(body, { status, headers });
    }
    // 200 with a streamed body — each stream element is one SSE event.
    case "Streaming":
      return sseResponse(result.stream);
    // 200 with a raw body — author-owned bytes with an explicit content-type.
    // No codec runs; the Uint8Array is written straight into the Response. The
    // `as BodyInit` cast satisfies TS 5.7, whose `Uint8Array<ArrayBufferLike>`
    // no longer matches DOM's `BufferSource` (it excludes SharedArrayBuffer) —
    // a bare Uint8Array is a valid body at runtime on both Workers and Node.
    case "Raw":
      return new Response(result.body as BodyInit, {
        status: 200,
        headers: { "content-type": result.contentType },
      });
    // 3xx — bodyless, with the target URL in the Location header.
    case "MovedPermanently":
    case "Found":
    case "SeeOther":
    case "TemporaryRedirect":
    case "PermanentRedirect":
      return new Response(null, { status, headers: { location: result.location } });
    // Error variants carrying an explanatory message — `{ error }` JSON body.
    case "BadRequest":
    case "Conflict":
    case "PayloadTooLarge":
    case "UnsupportedMediaType":
    case "UnprocessableEntity":
    case "TooManyRequests":
    case "UnavailableForLegalReasons":
    case "ServerError":
    case "NotImplemented":
    case "BadGateway":
    case "ServiceUnavailable":
    case "GatewayTimeout":
      return new Response(JSON.stringify({ error: result.message }), {
        status,
        headers: { "content-type": "application/json" },
      });
    // Self-describing statuses — bodyless.
    case "NoContent":
    case "Unauthorized":
    case "Forbidden":
    case "NotFound":
    case "MethodNotAllowed":
    case "NotAcceptable":
    case "RequestTimeout":
    case "Gone":
    case "LengthRequired":
      return new Response(null, { status });
  }
}

// v0.139 (ADR 0162): rebuild a `GET` response as its `HEAD` answer — identical
// status and headers, empty body (RFC 9110 §9.3.2). `Response.body` is not
// re-read: passing `null` discards the original body without consuming it, so a
// `Streaming` (`SSE`) `GET` answered as `HEAD` returns the stream's headers while
// its `ReadableStream` is never started or drained. `content-length` is omitted
// (the body is never materialised — permitted, §9.3.2 "MAY").
export function headResponse(response: Response): Response {
  return new Response(null, { status: response.status, headers: response.headers });
}

// v0.131 (ADR 0159): the cross-origin (CORS) policy a `from http` service carries
// via its `cors { }` section. The compiler synthesises one of these per
// CORS-enabled service and threads it into the entry router. `allowMethods` is
// derived from the service's routes at emit time (never restated by the author);
// `allowHeaders` carries the resolved list (the `content-type`/`Authorization`
// default, or the author's override). `maxAgeSecs` is the `Access-Control-Max-Age`
// in whole seconds, absent when the author gave no `maxAge`.
export interface CorsPolicy {
  readonly origins: readonly string[];
  readonly allowMethods: readonly string[];
  readonly allowHeaders: readonly string[];
  readonly credentials: boolean;
  readonly maxAgeSecs: number | null;
}

// Resolve the `Access-Control-Allow-Origin` value for a request, given the
// policy and the request's `Origin` header. A wildcard policy (`["*"]`) answers
// every origin with `*` (and needs no `Vary`, since the response does not depend
// on the request origin). A concrete allowlist **reflects** the request's origin
// when it matches — never echoes an unvalidated value — and returns `null` on no
// match, so the caller omits the header and the browser fails the request closed.
function corsResolveOrigin(policy: CorsPolicy, requestOrigin: string | null): string | null {
  if (policy.origins.length === 1 && policy.origins[0] === "*") return "*";
  if (requestOrigin !== null && policy.origins.includes(requestOrigin)) return requestOrigin;
  return null;
}

// Stamp the CORS response headers onto an already-built `Response`, in place.
// `Response.headers` is mutable, so this works uniformly across every
// `httpResultToResponse` shape — JSON, `Raw` bytes, a redirect, an error body,
// or an SSE `ReadableStream` — without reconstructing the response or touching
// its body. When the origin does not match the allowlist, no ACAO is added (the
// browser blocks the read); a reflected origin also gets `Vary: Origin` so a
// shared cache never serves one origin's grant to another.
export function applyCors<R extends Response>(
  response: R,
  policy: CorsPolicy,
  requestOrigin: string | null,
): R {
  const allowOrigin = corsResolveOrigin(policy, requestOrigin);
  if (allowOrigin === null) return response;
  response.headers.set("access-control-allow-origin", allowOrigin);
  if (allowOrigin !== "*") response.headers.append("vary", "Origin");
  if (policy.credentials) response.headers.set("access-control-allow-credentials", "true");
  return response;
}

// Build the `204 No Content` preflight response for an `OPTIONS` request against
// a CORS-enabled route. Answered by the entry router *before* the auth seam — a
// preflight is credential-less by spec, so it must not be rejected by a `by`
// actor / Bearer check. A non-allowlisted origin still gets a bodyless `204`, but
// without the `Access-Control-*` grant, so the browser's preflight check fails
// closed.
export function corsPreflightResponse(policy: CorsPolicy, requestOrigin: string | null): Response {
  const headers = new Headers();
  const allowOrigin = corsResolveOrigin(policy, requestOrigin);
  if (allowOrigin !== null) {
    headers.set("access-control-allow-origin", allowOrigin);
    if (allowOrigin !== "*") headers.append("vary", "Origin");
    if (policy.credentials) headers.set("access-control-allow-credentials", "true");
    headers.set("access-control-allow-methods", policy.allowMethods.join(", "));
    headers.set("access-control-allow-headers", policy.allowHeaders.join(", "));
    if (policy.maxAgeSecs !== null) {
      headers.set("access-control-max-age", String(policy.maxAgeSecs));
    }
  }
  return new Response(null, { status: 204, headers });
}

// v0.141 (ADR 0164): the security-headers policy a `from http` service carries
// via its `security { }` section. Unlike CORS, the compiler synthesises one of
// these for *every* `from http` service (not only those with a block), because
// the safe header is on by default — a service with no `security { }` still gets
// `{ nosniff: true, hstsMaxAgeSecs: null }`. `nosniff` stamps
// `X-Content-Type-Options: nosniff` (a JSON/text body a browser must not
// MIME-sniff into HTML); `hstsMaxAgeSecs`, when non-null, stamps
// `Strict-Transport-Security: max-age=…` (the opt-in HTTPS pin).
export interface SecurityPolicy {
  readonly nosniff: boolean;
  readonly hstsMaxAgeSecs: number | null;
}

// Stamp the security response headers onto an already-built `Response`, in place
// — the `applyCors` shape. `Response.headers` is mutable, so this works
// uniformly across every response family (JSON, `Raw` bytes, a redirect, an
// error body, an SSE stream, and the synthesised preflight / `405` / `304`).
// Composes with `applyCors`: the two set **disjoint** headers, so stamping order
// is irrelevant. `set` (not `append`) is idempotent and last-writer-wins, so a
// header also set at the edge (e.g. Cloudflare) is simply overwritten with the
// policy's value.
export function applySecurityHeaders<R extends Response>(
  response: R,
  policy: SecurityPolicy,
): R {
  if (policy.nosniff) response.headers.set("x-content-type-options", "nosniff");
  if (policy.hstsMaxAgeSecs !== null) {
    response.headers.set("strict-transport-security", `max-age=${policy.hstsMaxAgeSecs}`);
  }
  return response;
}

// v0.140 (ADR 0163): a **weak** validator over a response's serialised body, from
// a fast non-cryptographic hash (FNV-1a, 32-bit). Weak (`W/"…"`) is correct here —
// the guarantee is *semantic* equivalence of the representation, which weak
// validators exist for, and Bynk serves no byte-range requests (the one place a
// strong validator is required). Synchronous, so `httpResultToResponse` stays sync
// (no `await crypto.subtle`); a stronger hash (SHA-256) is a drop-in swap behind
// this name if ever needed.
export function weakETag(body: string): string {
  // FNV-1a over the string's UTF-16 code units. The exact byte basis is immaterial
  // to correctness — a validator only has to *change when the body changes* — so
  // hashing code units directly avoids a `TextEncoder` allocation on the hot path.
  let hash = 0x811c9dc5;
  for (let i = 0; i < body.length; i++) {
    hash ^= body.charCodeAt(i);
    // `* 16777619` (the FNV prime) in 32-bit via `Math.imul`.
    hash = Math.imul(hash, 0x01000193);
  }
  // `>>> 0` to an unsigned 32-bit; base-36 for a compact opaque token.
  return `W/"${(hash >>> 0).toString(36)}"`;
}

// v0.140 (ADR 0163): stamp a `Cache-Control` freshness directive onto an
// already-built `Response`, in place — the opt-in half of caching, driven by a
// handler's `@cache(maxAge:, scope:)` annotation. `scope` is the author's
// `public`/`private` (default `private`, so a *shared* cache never stores unless
// the author opts in); `maxAgeSecs` is `maxAge` in whole seconds. Only called for
// a `GET` that carries `@cache`; a route without it emits no `Cache-Control` and
// stays revalidatable through its `ETag` alone.
export function applyCache<R extends Response>(
  response: R,
  maxAgeSecs: number,
  scope: "public" | "private",
): R {
  response.headers.set("cache-control", `${scope}, max-age=${maxAgeSecs}`);
  return response;
}

// v0.140 (ADR 0163): the conditional-`GET` router decision. When the built
// response carries a weak `ETag` and the request's `If-None-Match` lists a
// matching validator, answer `304 Not Modified` with an empty body, copying the
// `ETag` and any `Cache-Control` across (RFC 9110 §15.4.5 — a `304` carries the
// caching headers a `200` would have). Otherwise the response passes through
// untouched. Like the CORS preflight and the method-semantics `405`, the `304` is
// a *router* response synthesised from the request validator, never an
// `HttpResult` variant (ADR 0126 D4). Runs *after* the handler (the body must
// exist to hash), so it saves bandwidth, not server work.
export function notModifiedIfMatch<R extends Response>(
  response: R,
  request: Request,
): R | Response {
  const etag = response.headers.get("etag");
  if (etag === null) return response;
  const ifNoneMatch = request.headers.get("if-none-match");
  if (ifNoneMatch === null) return response;
  if (!ifNoneMatchMatches(ifNoneMatch, etag)) return response;
  const headers = new Headers();
  headers.set("etag", etag);
  const cacheControl = response.headers.get("cache-control");
  if (cacheControl !== null) headers.set("cache-control", cacheControl);
  return new Response(null, { status: 304, headers });
}

// RFC 9110 §13.1.2: `If-None-Match` is a comma-separated list of entity tags (or a
// bare `*`, matching any current representation), compared with the **weak**
// comparison function — the `W/` prefix is ignored on both sides. Our validators
// are always weak and a conditional client echoes back exactly what we sent, but
// handling the list, `*`, and weak comparison keeps this correct against any
// conformant client.
function ifNoneMatchMatches(headerValue: string, etag: string): boolean {
  const target = stripWeakPrefix(etag);
  for (const raw of headerValue.split(",")) {
    const candidate = raw.trim();
    if (candidate === "*") return true;
    if (stripWeakPrefix(candidate) === target) return true;
  }
  return false;
}

function stripWeakPrefix(tag: string): string {
  return tag.startsWith("W/") ? tag.slice(2) : tag;
}

// v0.44: QueueResult — the built-in queue verdict sum (non-generic). `Ack`
// confirms the message; `Retry` redelivers it, carrying a reason for the log.
export type QueueResult =
  | { readonly tag: "Ack" }
  | { readonly tag: "Retry"; readonly reason: string };

export const QueueResult = {
  Ack: { tag: "Ack" } as QueueResult,
  Retry: (reason: string): QueueResult => ({ tag: "Retry", reason }),
};

// v0.9.2: agent instantiation + per-key state lifecycle.
//
// An agent keyed by some value lowers to a *lookup-or-create* of the durable
// state for that key. In bundle mode the per-agent `StateRegistry` holds an
// in-memory `DurableObjectState` per serialised key (same key → same state
// within a session; reset per test). In workers mode the agent is a Durable
// Object: `makeWorkersAgent` returns a typed proxy over the DO stub whose
// method calls route through `callDurableObjectMethod`. `makeAgent` picks the
// path: a present binding means workers, an absent one means bundle.

// A minimal structural view of the Cloudflare Durable Object namespace/stub
// surface. The real runtime is richer but structurally compatible.
export interface DurableObjectStub {
  fetch(input: string, init?: unknown): Promise<Response>;
}

export interface DurableObjectNamespace {
  idFromName(name: string): unknown;
  get(id: unknown): DurableObjectStub;
}

// Serialise an agent key to a stable string. Two semantically-equal keys must
// serialise identically: a string is itself, a primitive is its JSON form, a
// record is canonical JSON with sorted fields.
export function serialiseAgentKey(value: unknown): string {
  if (typeof value === "string") return value;
  if (typeof value === "number" || typeof value === "boolean" || value === null) {
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    return JSON.stringify(value.map((v) => serialiseAgentKey(v)));
  }
  if (typeof value === "object") {
    const obj = value as { [k: string]: unknown };
    const keys = Object.keys(obj).sort();
    return JSON.stringify(keys.map((k) => [k, serialiseAgentKey(obj[k])]));
  }
  return JSON.stringify(value);
}

// Per-agent registry: serialised key → in-memory durable state. Used in bundle
// mode (and in `bynkc test`). `reset()` clears every state so a fresh test
// sees a clean slate.
export class StateRegistry<K> {
  private states = new Map<string, DurableObjectState>();

  getOrCreate(key: K): DurableObjectState {
    const sk = serialiseAgentKey(key);
    let state = this.states.get(sk);
    if (state === undefined) {
      state = makeTestState(sk);
      this.states.set(sk, state);
    }
    return state;
  }

  reset(): void {
    this.states.clear();
  }
}

// Workers-mode agent method call: route through the DO stub's `fetch` under
// the `/_bynk/agent/<method>` wire protocol.
export async function callDurableObjectMethod<R>(
  stub: DurableObjectStub,
  method: string,
  args: unknown[],
  deps: unknown,
): Promise<R> {
  const response = await stub.fetch(`https://_bynk/_bynk/agent/${method}`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ args, deps }),
  });
  if (!response.ok) throw new Error(await response.text());
  return (await response.json()) as R;
}

// Workers-mode agent: a typed proxy over the DO stub. Each method access
// returns a function that splits its final argument off as `deps` and routes
// the rest as method args through `callDurableObjectMethod`. The `as C` cast
// gives call sites the agent's real method signatures, so user code reads
// identically to bundle mode.
export function makeWorkersAgent<C>(binding: DurableObjectNamespace, key: unknown): C {
  const stub = binding.get(binding.idFromName(serialiseAgentKey(key)));
  const proxy = new Proxy(
    {},
    {
      get(_target, prop: string | symbol) {
        if (typeof prop !== "string") return undefined;
        return (...callArgs: unknown[]) => {
          const deps = callArgs.length > 0 ? callArgs[callArgs.length - 1] : {};
          const args = callArgs.length > 0 ? callArgs.slice(0, -1) : [];
          return callDurableObjectMethod(stub, prop, args, deps);
        };
      },
    },
  );
  return proxy as C;
}

// Single agent-construction helper. A present DO binding selects the workers
// path; otherwise the bundle registry path. Call sites are identical across
// targets.
export function makeAgent<C>(
  registry: StateRegistry<unknown>,
  binding: DurableObjectNamespace | undefined,
  key: unknown,
  constructBundle: (state: DurableObjectState) => C,
): C {
  if (binding !== undefined) {
    return makeWorkersAgent<C>(binding, key);
  }
  const state = registry.getOrCreate(key);
  return constructBundle(state);
}

// v0.16: an in-process Durable-Object namespace for multi-Worker integration
// tests. `construct` builds the emitted DO class from a fresh in-memory state;
// one instance is kept per key (so state accumulates within a test case). The
// returned stub bridges the stub-side `fetch(url, init)` the agent runtime
// speaks to the DO class's server-side `fetch(request)`.
export function makeIntegrationDoNamespace(
  construct: (state: DurableObjectState) => { fetch(request: Request): Promise<Response> },
): DurableObjectNamespace {
  const instances = new Map<string, { fetch(request: Request): Promise<Response> }>();
  return {
    idFromName(name: string): unknown {
      return name;
    },
    get(id: unknown): DurableObjectStub {
      const k = String(id);
      let inst = instances.get(k);
      if (inst === undefined) {
        inst = construct(makeTestState(k));
        instances.set(k, inst);
      }
      const target = inst;
      return {
        fetch: (input: string, init?: unknown): Promise<Response> =>
          target.fetch(new Request(input, init as RequestInit)),
      };
    },
  };
}

// v0.47: Bearer-token verification (the actors slice-2 seam). Verifies a JWT's
// HS256 signature against `secret` using WebCrypto (constant-time
// `crypto.subtle.verify`), enforces `exp`/`nbf`, and returns the `sub` claim.
// Any failure is an `Err` the caller maps to 401 — fail-closed. The raw token
// never leaves this function; only the verified `sub` flows out.
function __bynkB64UrlToBytes(s: string): Uint8Array {
  const b64 = s.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(s.length / 4) * 4, "=");
  const bin = atob(b64);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

export async function verifyBearerJwtHs256(
  token: string,
  secret: string,
): Promise<Result<{ readonly sub: string; readonly claims: Record<string, unknown> }, string>> {
  const parts = token.split(".");
  if (parts.length !== 3) return Err("malformed token");
  const [headerB64, payloadB64, sigB64] = parts;
  let header: { alg?: unknown };
  try {
    header = JSON.parse(new TextDecoder().decode(__bynkB64UrlToBytes(headerB64)));
  } catch {
    return Err("malformed header");
  }
  // Reject algorithm confusion / `alg: none` — this seam only verifies HS256.
  if (header.alg !== "HS256") return Err("unsupported alg");
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    enc.encode(secret) as BufferSource,
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"],
  );
  let ok: boolean;
  try {
    ok = await crypto.subtle.verify(
      "HMAC",
      key,
      __bynkB64UrlToBytes(sigB64) as BufferSource,
      enc.encode(`${headerB64}.${payloadB64}`) as BufferSource,
    );
  } catch {
    return Err("verify failed");
  }
  if (!ok) return Err("bad signature");
  let payload: { sub?: unknown; exp?: unknown; nbf?: unknown };
  try {
    payload = JSON.parse(new TextDecoder().decode(__bynkB64UrlToBytes(payloadB64)));
  } catch {
    return Err("malformed payload");
  }
  const now = Math.floor(Date.now() / 1000);
  // RFC 7519: `exp`/`nbf` are NumericDate (a number). A present-but-non-number
  // claim is malformed — reject rather than silently skip the time check.
  if (payload.exp !== undefined && typeof payload.exp !== "number") return Err("malformed exp");
  if (payload.exp !== undefined && (payload.exp as number) < now) return Err("token expired");
  if (payload.nbf !== undefined && typeof payload.nbf !== "number") return Err("malformed nbf");
  if (payload.nbf !== undefined && (payload.nbf as number) > now) return Err("token not yet valid");
  if (typeof payload.sub !== "string" || payload.sub.length === 0) return Err("missing sub");
  // v0.53: surface the full verified claims for refinement-actor authorisation
  // (`actor Admin = User where hasClaim(...)`). The identity stays `sub`-minted
  // and sealed; claims are an authorisation-time input, checked at the boundary.
  return Ok({ sub: payload.sub, claims: payload as Record<string, unknown> });
}

// v0.51: Signature (webhook) verification — recompute an HMAC-SHA256 over the
// raw body (or `<timestamp>.<body>` when a timestamp is bound) and compare it,
// constant-time (`crypto.subtle.verify`), against the request's signature
// header (accepting a `sha256=<hex>` prefix or a bare hex digest). When a
// timestamp is bound, it is signed (so it cannot be forged) and checked within
// `toleranceSecs` for replay defence. Returns `true` iff the request is
// authentic; the caller maps `false` to 401. The body never reaches the handler
// unverified.
function __bynkHexToBytes(hex: string): Uint8Array {
  const clean = hex.startsWith("sha256=") ? hex.slice(7) : hex;
  if (clean.length === 0 || clean.length % 2 !== 0 || /[^0-9a-fA-F]/.test(clean)) {
    return new Uint8Array(0);
  }
  const out = new Uint8Array(clean.length / 2);
  for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
  return out;
}

export async function verifySignatureHmacSha256(
  body: string,
  secret: string,
  signatureHeader: string | null,
  timestamp: string | null,
  toleranceSecs: number | null,
): Promise<boolean> {
  if (signatureHeader === null) return false;
  // When a timestamp is bound it is part of the signed string (so it cannot be
  // forged); a tolerance, if set, bounds replay.
  let signingString = body;
  if (timestamp !== null) {
    const ts = Number(timestamp);
    if (!Number.isFinite(ts)) return false;
    if (toleranceSecs !== null) {
      const now = Math.floor(Date.now() / 1000);
      if (Math.abs(now - ts) > toleranceSecs) return false;
    }
    signingString = `${timestamp}.${body}`;
  }
  const sigBytes = __bynkHexToBytes(signatureHeader);
  if (sigBytes.length === 0) return false;
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    enc.encode(secret) as BufferSource,
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"],
  );
  try {
    return await crypto.subtle.verify(
      "HMAC",
      key,
      sigBytes as BufferSource,
      enc.encode(signingString) as BufferSource,
    );
  } catch {
    return false;
  }
}

// v0.102 (real-time track slice 2): the runtime contract for a held
// `Connection[F]`. A `Connection<F>` is a typed handle to a long-lived channel;
// `send` writes a server→client frame, `close` ends the connection. The
// concrete implementations — a capture-and-inspect `TestConnection` for the
// bundle/test target and the hibernatable-WebSocket binding for Workers — arrive
// with the `from WebSocket` protocol (slice 3); the language emits against this
// interface, and the linearity discipline (§2.9) governs ownership at the type
// level.

export interface Connection<F> {
  send(frame: F): Promise<void>;
  close(): Promise<void>;
}

// v0.103: the bundle/test realisation of a `Connection[F]` — a capture-and-inspect
// channel. The runtime-managed lifecycle in production becomes an inspectable
// record in tests (design notes §20): `sent` holds every frame `send`-given, and
// `closed` records disposal. A test drives an `on open` handler with a
// `TestConnection` and asserts what the agent sent.
export class TestConnection<F> implements Connection<F> {
  readonly sent: F[] = [];
  closed = false;

  async send(frame: F): Promise<void> {
    if (this.closed) throw new Error("send on a closed TestConnection");
    this.sent.push(frame);
  }

  async close(): Promise<void> {
    this.closed = true;
  }
}

// v0.104/v0.105 (real-time track slice 3b): the Cloudflare Workers realisation of
// a `Connection[F]`, wrapping a server-side `WebSocket` accepted in a Durable
// Object. A frame is sent as JSON; `close` ends the socket. The `connId` is the
// hibernation tag the socket was accepted under (slice 3b-ii): a stored connection
// persists this id, and `resolveConnection` re-presents the live socket by it
// after the DO wakes — so the held value survives eviction (§2.9.6).
export class WorkersConnection<F> implements Connection<F> {
  // Explicit fields + assignment, not constructor parameter properties — Node's
  // `--experimental-strip-types` (the `--inspect` debug path runs the emitted `.ts`
  // directly) rejects parameter properties, which are not erasable.
  private readonly ws: { send(data: string): void; close(): void };
  readonly connId: string;

  constructor(ws: { send(data: string): void; close(): void }, connId: string) {
    this.ws = ws;
    this.connId = connId;
  }

  async send(frame: F): Promise<void> {
    this.ws.send(JSON.stringify(frame));
  }

  async close(): Promise<void> {
    this.ws.close();
  }
}

// v0.105 (slice 3b-ii): the connection id a held `Connection` persists. The stored
// value is this string, not the live socket (which cannot be serialised); the
// socket is re-resolved per access via `resolveConnection`.
export function connIdOf(conn: Connection<unknown>): string {
  return (conn as WorkersConnection<unknown>).connId;
}

// v0.104 (real-time track slice 3b): the minimal structural surface of a
// Cloudflare server-side `WebSocket`, so emitted Worker code type-checks under
// `tsc --strict` without depending on `@cloudflare/workers-types`. The real
// runtime object is richer but compatible.
export interface WorkersWebSocket {
  accept(): void;
  send(data: string): void;
  close(code?: number, reason?: string): void;
  // v0.105 (slice 3b-ii): hibernation attachment — small per-socket data the
  // platform preserves across hibernation (the connId, so a waking dispatch can
  // recover it without loading durable state).
  serializeAttachment(value: unknown): void;
  deserializeAttachment(): unknown;
}

// v0.105 (slice 3b-ii): the Durable Object's hibernatable-WebSocket surface.
// `acceptWebSocket(ws, tags)` accepts a server socket so it survives the DO's
// hibernation (unlike `ws.accept()`); `getWebSockets(tag)` re-presents the
// accepted sockets (by tag) when the DO wakes. Narrowly typed and reached by a
// cast so the shared `DurableObjectState` (and the bundle `makeTestState`) need
// not carry the API.
interface HibernatableState {
  acceptWebSocket(ws: WorkersWebSocket, tags?: string[]): void;
  getWebSockets(tag?: string): WorkersWebSocket[];
}

// Accept a server socket into the DO under a fresh connection id (the hibernation
// tag), attach the id for wake-time recovery, and wrap it. The returned
// `WorkersConnection` carries the connId a held store persists.
export function acceptHibernatableConnection<F>(
  state: DurableObjectState,
  server: WorkersWebSocket,
  // v0.106 (slice 3b-iii): when the service has an inbound/close handler, the
  // sender identity + route args are attached too, so a waking `webSocketMessage`/
  // `webSocketClose` recovers them (the `on open` already authenticated the actor).
  meta?: Record<string, unknown>,
): WorkersConnection<F> {
  const connId = crypto.randomUUID();
  (state as unknown as HibernatableState).acceptWebSocket(server, [connId]);
  server.serializeAttachment(meta === undefined ? { connId } : { connId, ...meta });
  return new WorkersConnection<F>(server, connId);
}

// Re-present the live socket a stored connId names, re-wrapped as a
// `WorkersConnection`. `None` when the platform has no such socket — a connection
// that has since closed (normal lifecycle, not corruption).
export function resolveConnection<F>(
  state: DurableObjectState,
  connId: string,
): Option<WorkersConnection<F>> {
  const sockets = (state as unknown as HibernatableState).getWebSockets(connId);
  const ws = sockets[0];
  return ws === undefined ? None : Some(new WorkersConnection<F>(ws, connId));
}

export interface WorkersWebSocketPair {
  client: WorkersWebSocket;
  server: WorkersWebSocket;
}

// Construct a Cloudflare `WebSocketPair` (a Workers runtime global), returned as
// a named `{ client, server }` pair. The pair is index-shaped at runtime
// (`pair[0]` = client, `pair[1]` = server); this normalises it and keeps the
// global access in one place so emitted code stays free of ambient declarations.
export function newWebSocketPair(): WorkersWebSocketPair {
  const Ctor = (globalThis as { WebSocketPair?: new () => { 0: WorkersWebSocket; 1: WorkersWebSocket } })
    .WebSocketPair;
  if (Ctor === undefined) {
    throw new Error("WebSocketPair is not available in this runtime");
  }
  const pair = new Ctor();
  return { client: pair[0], server: pair[1] };
}

// Build the `101 Switching Protocols` response that hands the client end of an
// accepted `WebSocketPair` back to the caller — the Cloudflare upgrade
// completion. `webSocket` is a Workers-specific `ResponseInit` extension.
export function webSocketUpgradeResponse(client: WorkersWebSocket): Response {
  return new Response(null, { status: 101, webSocket: client } as ResponseInit & {
    webSocket: WorkersWebSocket;
  });
}
