{"version":3,"sources":["../src/feed.ts"],"sourcesContent":["// feedFetch — the connector's egress, and the difference between it and `hostFetch`\n// is the whole of D2 (`reckoner/docs/specs/CONNECTOR_EGRESS_FIXING_SPEC.md` §2).\n//\n// `hostFetch(url, init)` takes a URL. The host checks it against your effective\n// allowlist, but *within* that allowlist your app picks which host, which path and\n// what body, on every call. That is fine for an app whose logic you control.\n//\n// It is not fine for a **connector** — an app whose job is to pump bytes from a feed\n// into a document, where the bytes it fetched are, in effect, a program steering it\n// (`REPORTING_SPREADSHEET §3.2` RB-1). An allowlist bounds the host *set*; it does\n// nothing about per-call choice inside that set, and nothing at all about the body.\n//\n// So `feedFetch` does not take a URL. It takes a **feed-instance id** and a typed\n// **param object**, and the host constructs the request from a template it compiled,\n// at grant time, from your app's trusted feed configuration. Your code cannot name a\n// target, so content your code just read cannot name one either.\n//\n// What that means in practice when you write a connector:\n//\n//   • the origin, path and method come from your manifest's `feed:fetch` config —\n//     not from this call, and not from anything you fetched;\n//   • `params` fill only the slots the template declared, and each is typed\n//     (an ISO-8601 instant, a bounded integer, or one of a declared enum). A value\n//     that is not what the slot declared is rejected — including, incidentally,\n//     anything URL-shaped;\n//   • **pagination is not yours.** The host mints the cursor, keeps it, and spends it;\n//     you neither supply nor receive one. A cursor you round-tripped would be either\n//     bytes you authored or a function of bytes you fetched, and both are exactly what\n//     this surface exists to prevent;\n//   • a `POST`/body feed's body comes from the template too. There is no parameter\n//     that carries body bytes.\n//\n// Hold `feed:fetch` **instead of** `net:fetch`, not alongside it. An app holding\n// `net:fetch` has the URL surface back regardless of any template it was also given,\n// which is why they are two capabilities and not one with a flag.\n\nimport { protocolRequest } from './sandboxUtils';\nimport { throwOnRefusal } from './protocolRefusal';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_FEED } from './generated/protocol';\n\n/** Values a feed template's declared slots accept. The host validates each against\n *  the slot's declared type; anything else is `invalid-params`. */\nexport type FeedParams = Record<string, string | number>;\n\n/** The serialized response from {@link feedFetch} — the same shape `hostFetch`\n *  returns, because reading a feed should feel like reading a fetch. The difference\n *  is entirely in what you may ASK for. */\nexport interface FeedFetchResponse {\n  status: number;\n  statusText: string;\n  headers: Record<string, string>;\n  body: string;\n  /** True if the body hit the host's size cap and was truncated. */\n  truncated: boolean;\n}\n\n/**\n * Fire one of your app's configured feeds through the host.\n *\n * ```ts\n * // `instanceId` comes from the host when the feed is launched — it is opaque,\n * // host-minted, and bound to your app; you never construct one.\n * const res = await feedFetch(instanceId, { since: '2026-08-01T00:00:00Z' });\n * const rows = JSON.parse(res.body);\n * ```\n *\n * A reachable server's reply — including a non-2xx status — RESOLVES; inspect\n * `.status`. Everything else REJECTS with an {@link Error} carrying a machine `.code`:\n *\n * - `forbidden` — you do not hold `feed:fetch`, or `instanceId` is not one of yours.\n *   **The two are deliberately indistinguishable**, so this is not an oracle for which\n *   feeds exist.\n * - `invalid-params` — a param the template does not declare, a value that is not what\n *   its slot declared, or a body that would exceed the template's cap. Naming a cursor\n *   slot lands here too: the cursor is the host's, not a parameter.\n * - `budget` — this instance's request budget is spent. It bounds runaway loops; it is\n *   a tripwire, not containment.\n * - `unsupported` — the host's pinned egress path is unavailable. `feedFetch`\n *   deliberately has **no fallback**: the alternative path does no DNS resolution and no\n *   socket pinning, and silently downgrading a credentialed feed onto it is the hazard\n *   this whole mechanism removes. A connector that cannot use the pinned path does not\n *   fetch.\n * - `blocked` / `redirect` / `too-large` / `network` — the same server-side SSRF,\n *   per-hop redirect and size guards every proxied fetch meets.\n */\n// The parameter is spelled structurally rather than as `FeedParams`, deliberately: the\n// wire descriptor in `@immediately-run/sandbox-protocol` records the type that actually\n// crosses the boundary, and an SDK-local alias name in a CROSS-REPO wire contract would\n// describe this package rather than the protocol. `FeedParams` stays exported as the\n// name callers write.\nexport const feedFetch = async (\n  instanceId: string,\n  params: Record<string, string | number> = {},\n): Promise<FeedFetchResponse> => {\n  const res = (await protocolRequest(SCHEMES[PROTOCOL_FEED], 'fetch', [{ instanceId, params }])) as\n    | { ok: true; data: FeedFetchResponse }\n    | { ok: false; code?: string; message?: string }\n    | undefined;\n  throwOnRefusal(res, 'feedFetch failed');\n  return res.data;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAoCA,0BAAgC;AAChC,6BAA+B;AAC/B,6BAAwB;AACxB,sBAA8B;AAoDvB,MAAM,YAAY,OACvB,YACA,SAA0C,CAAC,MACZ;AAC/B,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,6BAAa,GAAG,SAAS,CAAC,EAAE,YAAY,OAAO,CAAC,CAAC;AAI5F,6CAAe,KAAK,kBAAkB;AACtC,SAAO,IAAI;AACb;","names":[]}