{"_id":"@blacklake-systems/surface-sdk","_rev":"5-76de7d1781ee04bf7136ab069ecda103","name":"@blacklake-systems/surface-sdk","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.5":{"name":"@blacklake-systems/surface-sdk","version":"0.1.5","keywords":["blacklake","agent","governance","control-plane","sdk"],"license":"MIT","_id":"@blacklake-systems/surface-sdk@0.1.5","maintainers":[{"name":"blacklake-team","email":"team@blacklake.systems"}],"homepage":"https://blacklake.systems/product","bugs":{"url":"https://blacklake.systems/support"},"dist":{"shasum":"2de041e5e3007254bcfa6e83be03b7dd33a37fd2","tarball":"https://registry.npmjs.org/@blacklake-systems/surface-sdk/-/surface-sdk-0.1.5.tgz","fileCount":7,"integrity":"sha512-ooRPwnAuBYp3qnIDfK5aDT9eW2BAN5HVNDOubwKShAJnPqZu8o2n4qjUDgdEuM7XNZWe50UcnSwJFU/HbkMCXA==","signatures":[{"sig":"MEYCIQDk3YmfGwO76lrO+0oNr3bgWjpi8qUxhcOM1t4hA6LKHAIhANxOdsNOSo08l8cxayFgZQaJaHxBns+cALcTlfVS97/i","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":47516},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"794c4d1993a08d5fadd2080356aae389246a505b","scripts":{"build":"tsc","prepublishOnly":"tsc"},"_npmUser":{"name":"blacklake-team","email":"team@blacklake.systems"},"repository":{"url":"git+https://github.com/blacklake-systems/control-plane.git","type":"git","directory":"packages/sdk"},"_npmVersion":"10.9.4","description":"TypeScript SDK for BlackLake Surface — agent governance infrastructure","directories":{},"_nodeVersion":"22.22.0","_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.8.3"},"_npmOperationalInternal":{"tmp":"tmp/surface-sdk_0.1.5_1775985789426_0.17208771538052248","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Use the unified package: npm i blacklake. (If this warning appears while installing \"blacklake\" itself, ignore it — this is an internal dep.)"},"0.2.0":{"name":"@blacklake-systems/surface-sdk","version":"0.2.0","keywords":["blacklake","agent","governance","control-plane","sdk"],"license":"MIT","_id":"@blacklake-systems/surface-sdk@0.2.0","maintainers":[{"name":"blacklake-team","email":"team@blacklake.systems"}],"homepage":"https://blacklake.systems/product","bugs":{"url":"https://blacklake.systems/support"},"dist":{"shasum":"822742246731f319843afcb57e860895d35bcf24","tarball":"https://registry.npmjs.org/@blacklake-systems/surface-sdk/-/surface-sdk-0.2.0.tgz","fileCount":15,"integrity":"sha512-h67fl6Ekj1IGoz/KMgqaFpq3QLQsJ/bP6tI30Mqyv2tDffIU+UW9P6FTpdOPoUqVH79dpOppoL9XPBZWT5Mdsw==","signatures":[{"sig":"MEUCIQDJQ7X+yXkVJsAWDhYsEm3SpZOUjLz1YOe9WZkijsXhEgIgfAneF8/Z8vTlkfsHwC5jByGmyjz07J1B6xv2ZrFmlDY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":101447},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"d3400dab22e8038d4f3c510054bce8eea8ef5788","scripts":{"test":"vitest run","build":"tsc","prepublishOnly":"tsc"},"_npmUser":{"name":"blacklake-team","email":"team@blacklake.systems"},"repository":{"url":"git+https://github.com/blacklake-systems/control-plane.git","type":"git","directory":"packages/sdk"},"_npmVersion":"10.9.4","description":"TypeScript SDK for BlackLake Surface — agent governance infrastructure","directories":{},"_nodeVersion":"22.22.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.1.1","typescript":"^5.8.3"},"_npmOperationalInternal":{"tmp":"tmp/surface-sdk_0.2.0_1777474033157_0.021117987435671504","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Use the unified package: npm i blacklake. (If this warning appears while installing \"blacklake\" itself, ignore it — this is an internal dep.)"},"0.3.0":{"name":"@blacklake-systems/surface-sdk","version":"0.3.0","keywords":["blacklake","agent","governance","control-plane","sdk"],"license":"MIT","_id":"@blacklake-systems/surface-sdk@0.3.0","maintainers":[{"name":"blacklake-team","email":"team@blacklake.systems"}],"homepage":"https://blacklake.systems/product","bugs":{"url":"https://blacklake.systems/support"},"dist":{"shasum":"24b954a1cb8b38fb30fd0d87266dc9cc4951d611","tarball":"https://registry.npmjs.org/@blacklake-systems/surface-sdk/-/surface-sdk-0.3.0.tgz","fileCount":27,"integrity":"sha512-tdS8lwueUrUBKD3D0tn5gECDrkuDuEucaC1nNIQsdyC00GnYJ/sARQ/cYieBbEC+orDfSf5qkkqzQ8G4K4QCYw==","signatures":[{"sig":"MEUCIQD0xAAUeJNNP4XR6xiPD/I7srrjgbYmXyOnl9WXkCKueQIgIR+J8Af+e/ZvTGE5Izr5RxzVawG4wDPIqE88E1CMciU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":247402},"main":"./dist/index.js","type":"module","_from":"file:blacklake-systems-surface-sdk-0.3.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc"},"_npmUser":{"name":"blacklake-team","email":"team@blacklake.systems"},"_resolved":"/private/var/folders/8c/y62dgrt959g_27z2kzzpgt440000gp/T/6d9914db04624fd8fee03638d27079b5/blacklake-systems-surface-sdk-0.3.0.tgz","_integrity":"sha512-tdS8lwueUrUBKD3D0tn5gECDrkuDuEucaC1nNIQsdyC00GnYJ/sARQ/cYieBbEC+orDfSf5qkkqzQ8G4K4QCYw==","repository":{"url":"git+https://github.com/blacklake-systems/control-plane.git","type":"git","directory":"packages/sdk"},"_npmVersion":"10.9.4","description":"TypeScript SDK for BlackLake Surface — control layer for AI actions","directories":{},"_nodeVersion":"22.22.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.1.1","typescript":"^5.8.3"},"_npmOperationalInternal":{"tmp":"tmp/surface-sdk_0.3.0_1778251322650_0.30480020635768","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Use the unified package: npm i blacklake. (If this warning appears while installing \"blacklake\" itself, ignore it — this is an internal dep.)"},"0.3.1":{"name":"@blacklake-systems/surface-sdk","version":"0.3.1","keywords":["blacklake","agent","governance","control-plane","sdk"],"license":"MIT","_id":"@blacklake-systems/surface-sdk@0.3.1","maintainers":[{"name":"blacklake-team","email":"team@blacklake.systems"}],"homepage":"https://blacklake.systems/product","bugs":{"url":"https://blacklake.systems/support"},"dist":{"shasum":"e647fd1d602590735be90987e3714c8936761563","tarball":"https://registry.npmjs.org/@blacklake-systems/surface-sdk/-/surface-sdk-0.3.1.tgz","fileCount":27,"integrity":"sha512-MIH1cid3MqdJBTUsI2SAvHaMFtK6habhHiC2ve/TFoOCN2LOWqlu3ydTRnVV2tBuvew9TUm/0hbH1BnIhYeOuQ==","signatures":[{"sig":"MEYCIQDIvQ3Mlpa0BxQvRyYTAWmqNSldkhRkC8mMGnzErGwS/QIhAOIQgRWfwM55PYGiA1pSEi/81FtYYqz3nvcCiFsLtX/z","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":281907},"main":"./dist/index.js","type":"module","_from":"file:blacklake-systems-surface-sdk-0.3.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc"},"_npmUser":{"name":"blacklake-team","email":"team@blacklake.systems"},"_resolved":"/private/var/folders/8c/y62dgrt959g_27z2kzzpgt440000gp/T/5999d3ebba812c0d40276ab4db8d34ef/blacklake-systems-surface-sdk-0.3.1.tgz","_integrity":"sha512-MIH1cid3MqdJBTUsI2SAvHaMFtK6habhHiC2ve/TFoOCN2LOWqlu3ydTRnVV2tBuvew9TUm/0hbH1BnIhYeOuQ==","repository":{"url":"git+https://github.com/blacklake-systems/control-plane.git","type":"git","directory":"packages/sdk"},"_npmVersion":"10.9.4","description":"TypeScript SDK for BlackLake Surface — control layer for AI actions","directories":{},"_nodeVersion":"22.22.0","_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.1.1","typescript":"^5.8.3","@types/node":"^22.14.0"},"_npmOperationalInternal":{"tmp":"tmp/surface-sdk_0.3.1_1778864320117_0.0434404777693842","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Use the unified package: npm i blacklake. (If this warning appears while installing \"blacklake\" itself, ignore it — this is an internal dep.)"}},"time":{"created":"2026-04-12T09:23:09.334Z","modified":"2026-05-15T17:01:11.296Z","0.1.5":"2026-04-12T09:23:09.561Z","0.2.0":"2026-04-29T14:47:13.290Z","0.3.0":"2026-05-08T14:42:02.841Z","0.3.1":"2026-05-15T16:58:40.298Z"},"bugs":{"url":"https://blacklake.systems/support"},"license":"MIT","homepage":"https://blacklake.systems/product","keywords":["blacklake","agent","governance","control-plane","sdk"],"repository":{"url":"git+https://github.com/blacklake-systems/control-plane.git","type":"git","directory":"packages/sdk"},"description":"TypeScript SDK for BlackLake Surface — control layer for AI actions","maintainers":[{"name":"blacklake-team","email":"team@blacklake.systems"}],"readme":"# @blacklake-systems/surface-sdk\n\n> **⚠️ Deprecated.** This package is now part of the unified [`blacklake`](https://www.npmjs.com/package/blacklake) npm package. Install `blacklake` and `import { govern } from 'blacklake'`. See the [migration doc](https://www.blacklake.systems/docs/migration-from-old-packages) for sed-style search-and-replace examples. This package will continue to ship as a thin re-export through the next two minor versions.\n\nTypeScript SDK for [BlackLake](https://www.blacklake.systems/product) — AI control infrastructure and analytics.\n\nUse this SDK when your code calls LLMs or tools directly (backend services, custom agents, batch jobs) and you want every consequential action on the same ledger as MCP proxy, CI, shell, cloud audit ingest, and Depth workflows. `bl.govern()` returns the decision, `bl.cost.record()` attributes spend, `bl.decisions.verify()` proves a receipt later.\n\n> **Note:** If you are routing tool calls through the MCP proxy, you do not need this SDK. The proxy handles governance automatically. Use the SDK when you want to call the governance API directly from your own code.\n\n## Install\n\n```bash\nnpm install @blacklake-systems/surface-sdk\n```\n\n## Quick Start\n\nSign up at [console.blacklake.systems](https://console.blacklake.systems) and grab your API key from the dashboard. Then:\n\n```typescript\nimport { BlackLake } from '@blacklake-systems/surface-sdk';\n\nconst bl = new BlackLake({ apiKey: process.env.BLACKLAKE_API_KEY! });\n\nconst decision = await bl.govern({\n  agent: 'my-bot',\n  tool: 'send_email',\n  action: { to: 'alice@example.com' },\n});\n\nswitch (decision.decision) {\n  case 'allow':\n    // safe to proceed\n    break;\n  case 'approval_required':\n    // wait for a human reviewer; decision.approval_id has the pending approval\n    break;\n  case 'deny':\n  case 'default_deny':\n    // not allowed. decision.reason explains why.\n    throw new Error(`BlackLake denied: ${decision.reason}`);\n}\n```\n\n`default_deny` is the fail-safe — it means no policy matched. Treat it the same as `deny` in your code; if you see it for a call you expected to allow, write a policy that matches the agent + tool selectors.\n\n`baseUrl` defaults to `https://api.blacklake.systems`. No further configuration needed for the cloud product.\n\n### Local Surface\n\nRun `npx @blacklake-systems/surface-cli` first to start Surface on your machine, then point the SDK at it:\n\n```typescript\nimport { BlackLake } from '@blacklake-systems/surface-sdk';\n\nconst bl = new BlackLake({\n  baseUrl: 'http://localhost:3100',\n  apiKey: process.env.BLACKLAKE_API_KEY!,\n});\n\n// Evaluate governance before executing a tool call\nconst result = await bl.govern({\n  agent: 'expense-bot',\n  tool: 'payments.send',\n  action: { amount: 4200, vendor: 'Acme Corp' },\n});\n\nif (result.decision === 'allow') {\n  // proceed with tool call\n}\n```\n\nOr use the hosted Surface console at console.blacklake.systems when you need shared policies, approvals, budgets, exports, and team visibility.\n\nPairs with [BlackLake Depth](https://www.npmjs.com/package/@blacklake-systems/depth-sdk) — the durable-execution runtime that survives crashes. Use Depth to run multi-step agent workflows; Surface evaluates each tool call inside them.\n\n## Response envelopes\n\nSingular responses (`GET /v1/agents/<id>`, `POST /v1/agents`, etc.) are content-negotiated.\n\n- **`Accept-Envelope: v2`** — server returns `{ data: <resource>, ...metadata }`. This applies to **all singular routes** that have been wired through `singularEnvelope()` server-side, not just agents.\n- **No header (legacy)** — server returns the bare resource and emits `BlackLake-Singular-Envelope: deprecated; send \"Accept-Envelope: v2\" to opt in to the new shape` so the caller knows to migrate.\n\nThe TS SDK ships `Accept-Envelope: v2` on every request and peels `.data` automatically, so SDK callers always see the resource directly. Direct HTTP callers (curl, custom clients) opt in with the header.\n\nList endpoints (`GET /v1/agents`, etc.) always return `{ data: T[], total, limit, offset, sort, order }` regardless of the header.\n\n## API Reference\n\n### `new BlackLake(config)`\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `apiKey` | `string` | — | Your BlackLake API key (required) |\n| `baseUrl` | `string` | `https://api.blacklake.systems` | API base URL. Override only for local development (e.g. `http://localhost:3100`). |\n\n### `bl.govern(request)`\n\nEvaluate whether an agent is allowed to invoke a tool.\n\n```typescript\nconst result = await bl.govern({\n  agent: 'expense-bot',       // agent name\n  tool: 'payments.send',      // tool name\n  action: { amount: 4200 },   // optional: tool invocation payload\n  context: { ip: '10.0.0.1' } // optional: request metadata\n});\n\n// result.decision: 'allow' | 'deny' | 'approval_required' | 'default_deny'\n// result.evaluation_id: string\n// result.policy_id: string | null\n// result.reason: string\n// result.evaluated_at: string (ISO 8601)\n// result.approval_id: string | undefined (set when decision === 'approval_required')\n// result.decision_token: string — HMAC-signed receipt binding (evaluation_id, decision).\n//   Quote this when reporting a governance outcome to a downstream operator — they can\n//   confirm via bl.decisions.verify(...) that the decision really came from this server,\n//   not from an LLM hallucinating a denial.\n```\n\n**Handle every decision explicitly.** `default_deny` is returned when no policy\nmatches *and* the agent has no binding for the tool — it is distinct from `deny`\n(an explicit deny policy matched). Treating it as a generic fallback is a\nfootgun:\n\n```typescript\nswitch (result.decision) {\n  case 'allow':              return await payments.send(payload);\n  case 'deny':               throw new Error(`blocked: ${result.reason}`);\n  case 'approval_required':  return awaitApproval(result.approval_id!);\n  case 'default_deny':       throw new Error(\n    `no matching policy or binding — register '${tool}' for '${agent}' or add an allow policy`,\n  );\n}\n```\n\n### `bl.agents`\n\n```typescript\nawait bl.agents.create({ name, environment, risk_classification, description?, approval_mode? });\nawait bl.agents.list({ environment?, status? });\nawait bl.agents.get(id);\nawait bl.agents.update(id, { name?, description?, environment?, risk_classification?, status?, approval_mode? });\nawait bl.agents.suspend(id);\nawait bl.agents.activate(id);\nawait bl.agents.bindTool(agentId, toolId);\nawait bl.agents.listTools(agentId);   // returns ToolBinding[] — each item has { binding_id, binding_created_at, tool: Tool }\nawait bl.agents.unbindTool(agentId, toolId);\n```\n\n### `bl.tools`\n\n```typescript\nawait bl.tools.create({ name, risk_classification, description? });\nawait bl.tools.list();\nawait bl.tools.get(id);\n```\n\n### `bl.policies`\n\n```typescript\nawait bl.policies.create({ name, priority, outcome, agent_selector?, tool_selector?, enabled? });\nawait bl.policies.list();\nawait bl.policies.get(id);\nawait bl.policies.update(id, { name?, priority?, outcome?, agent_selector?, tool_selector?, enabled? });\nawait bl.policies.delete(id);\n```\n\n### `bl.evaluations`\n\n```typescript\nawait bl.evaluations.list({ agent_id?, tool_id?, outcome?, limit?, offset? });\nawait bl.evaluations.get(id);\n```\n\n### Verifying decisions\n\nLLM agents can fabricate text that looks like a denial — `'BlackLake denied this tool call'` — without ever actually invoking the bridge. Decision tokens close that gap. Every honest `govern()` call returns an HMAC-signed token bound to `(evaluation_id, decision)`; a hallucinated token fails verification. Use `bl.decisions.verify(...)` whenever you're acting on a governance outcome reported by an agent rather than the API directly.\n\n```typescript\nimport { BlackLake } from '@blacklake-systems/surface-sdk';\nconst bl = new BlackLake({ apiKey: process.env.BLACKLAKE_API_KEY! });\n\nconst decision = await bl.govern({\n  agent: 'my-bot',\n  tool: 'send_email',\n  action: { to: 'alice@example.com' },\n});\n\n// Later (e.g. in an operator's audit tool, or a different process):\nconst verification = await bl.decisions.verify({\n  evaluation_id: decision.evaluation_id,\n  decision_token: decision.decision_token,\n});\n\nif (verification.valid) {\n  console.log('Confirmed: this was a real BlackLake decision', verification.decision);\n} else {\n  console.warn('Token did not verify:', verification.reason);\n}\n```\n\n### `bl.organisation`\n\n```typescript\nawait bl.organisation.get();                                   // fetch the current organisation (derived from the API key)\nawait bl.organisation.delete(confirmation, reason?);           // permanently delete the organisation; pass the organisation's exact name as confirmation\nawait bl.organisation.reset(confirmation, reason?);            // wipe operational data without deleting the workspace; preserves users, API keys, sessions, billing; rate-limited 3/hour\n```\n\n`reset()` returns `{ reset_at, organisation_id, total_rows, counts, preserved, note }` so tests can assert clean state. Same name-confirmation safeguard as `delete()`.\n\n### `bl.apiKeys`\n\n```typescript\nawait bl.apiKeys.list();              // returns { keys: ApiKey[] } — each item has { id, name, key_suffix, created_at, revoked_at }\nawait bl.apiKeys.create('prod-key');  // returns { id, name, key, created_at, warning } — the raw key is shown ONCE; store it securely\nawait bl.apiKeys.revoke(id);          // sets revoked_at on the key; the API rejects revoking the key in use\n```\n\n### `bl.approvals`\n\n```typescript\nawait bl.approvals.list({ status?, agent_id?, tool_id?, limit?, offset? });  // returns PaginatedResponse<Approval>\nawait bl.approvals.get(id);\nawait bl.approvals.status(id);                                               // returns ApprovalStatusResponse — lightweight poll target\nawait bl.approvals.approve(id, { decided_by, reason });\nawait bl.approvals.reject(id, { decided_by, reason });\nawait bl.approvals.breakGlass(id, { decided_by, reason });                  // emergency override; reason must be ≥ 40 chars and is surfaced in the audit trail\nawait bl.approvals.wait(id, { interval?, timeout? });                        // polls status until approved/rejected/expired; throws BlackLakeError on timeout\n```\n\nBoth `decided_by` and `reason` are **required** and must be non-empty strings (the server enforces `.min(1)`). The reason lands in the receipt and is surfaced to webhook subscribers and the console audit trail.\n\n`wait()` defaults to polling every **2 000 ms** with a **5-minute** total timeout,\nthen throws `BlackLakeError` with code `APPROVAL_WAIT_TIMEOUT` and HTTP status\n`408`. These defaults are sensible for an interactive approval queue; for\nhigh-frequency agents set a shorter `timeout` (ms) and handle the throw.\n\n```typescript\ntry {\n  const resolved = await bl.approvals.wait(result.approval_id!, { timeout: 30_000 });\n  if (resolved.status === 'approved') { /* proceed */ }\n  else                                 { /* rejected or expired — do NOT proceed */ }\n} catch (err) {\n  if (err instanceof BlackLakeError && err.code === 'APPROVAL_WAIT_TIMEOUT') {\n    // queue for later, page a human, or reject the original request\n  }\n  else throw err;\n}\n```\n\nReturns the fully-populated `Approval` once the status leaves `'pending'`.\n**Always branch on `resolved.status`** — `wait()` does not throw for `rejected`\nor `expired`; the caller must inspect the resolved record.\n\n### `bl.webhooks`\n\n```typescript\nawait bl.webhooks.list({ limit?, offset?, sortBy?, order? });               // returns ListResult<Webhook>\nawait bl.webhooks.create({ url, events, enabled? });                         // returns CreatedWebhook — the raw signing secret is shown ONCE; store it securely\nawait bl.webhooks.get(id);\nawait bl.webhooks.update(id, { url?, events?, enabled? });\nawait bl.webhooks.delete(id);\nawait bl.webhooks.listDeliveries(id, { limit?, offset? });                   // returns ListResult<WebhookDelivery>\nawait bl.webhooks.test(id);                                                  // fire a synthetic delivery to validate routing + signing\nawait bl.webhooks.resendDelivery(id, deliveryId);                            // re-fire one prior delivery (re-signed with current secret + fresh timestamp)\nawait bl.webhooks.resendFailedDeliveries(id);                                // bulk replay every failed delivery (capped at 100)\nawait bl.webhooks.health(id);                                                // success rate, p50/p95 latency, last error, consecutive-failure count\nawait bl.webhooks.deadLetter(id, { limit?, offset? });                       // deliveries explicitly tagged status='dead'\n```\n\nThe full event catalogue is:\n\n```typescript\ntype WebhookEvent =\n  | 'approval.created'\n  | 'approval.approved'\n  | 'approval.rejected'\n  | 'budget.threshold_crossed'\n  | 'budget.limit_exceeded'\n  | 'evaluation.created'\n  | 'evaluation.denied'\n  | 'evaluation.approval_required'\n  | 'cost.recorded'\n  | 'upstream.unhealthy'\n  | 'upstream.recovered';\n```\n\nEach request is signed with HMAC-SHA256 over `\"<timestamp>.<raw_body>\"`; the signature is sent in the `X-BlackLake-Signature` header (format: `sha256=<hex>`) and the millisecond timestamp in `X-BlackLake-Timestamp`.\n\n### Verifying webhook signatures\n\nAlways verify the signature before trusting a webhook payload. The SDK ships a\nconstant-time helper that uses the Web Crypto API (no Node `crypto` dependency,\nso it works in Cloudflare Workers, Deno, and browsers):\n\n```typescript\nimport { BlackLake, BlackLakeError } from '@blacklake-systems/surface-sdk';\n\n// In your webhook handler (Express example):\napp.post('/webhooks/blacklake', express.raw({ type: 'application/json' }), async (req, res) => {\n  try {\n    await BlackLake.verifyWebhookSignature({\n      secret: process.env.BLACKLAKE_WEBHOOK_SECRET!,\n      rawBody: req.body.toString('utf8'),          // the raw bytes, not JSON.stringify(req.body)\n      signature: req.header('x-blacklake-signature')!,\n      timestamp: req.header('x-blacklake-timestamp')!,\n    });\n  } catch (err) {\n    if (err instanceof BlackLakeError && err.code === 'WEBHOOK_SIGNATURE_INVALID') {\n      return res.status(401).end();\n    }\n    throw err;\n  }\n  // signature verified — safe to parse the body and act on it\n  const event = JSON.parse(req.body.toString('utf8'));\n  res.status(204).end();\n});\n```\n\nRejects on length mismatch, wrong prefix, or signature mismatch. Constant-time\ncomparison is used to avoid timing side-channels.\n\n### `bl.cost`\n\nCost attribution, summaries, and pricing-catalogue inspection. Use `record()` whenever a call ran outside the proxy paths so spend stays on the same ledger as governance.\n\n```typescript\nawait bl.cost.record({                                                       // attribute one LLM call's spend back to an evaluation\n  evaluation_id,\n  agent, tool,\n  provider, model,\n  input_tokens, output_tokens,\n  capture_path,                                                              // CapturePath enum, see below\n});\nawait bl.cost.estimate({ provider, model, input_tokens, output_ceiling_tokens });   // pre-call estimate; feed into bl.govern({ estimate: ... })\nawait bl.cost.summary(period);                                               // 'day' | '7d' | '30d' | '90d' (default '30d')\nawait bl.cost.timeseries(period);                                            // one row per day for the spend chart\nawait bl.cost.decomposition(period);                                         // agent → tool → model → cost-component tree\nawait bl.cost.byEvaluation(evaluationId);                                    // every cost record bound to one evaluation + v2 decision token\nawait bl.cost.export('csv' | 'ndjson', period);                              // streaming export — pipe into jq, BigQuery, S3\nawait bl.cost.pricing(version?);                                             // priced-model catalogue with exact/prefix match tags\nawait bl.cost.orphans({ limit?, offset?, since? });                          // cost records not linked to a governed evaluation — coverage gaps\n```\n\n`CapturePath` enum:\n\n```typescript\ntype CapturePath =\n  | 'manual' | 'mcp' | 'sdk' | 'ci' | 'shell'\n  | 'cloud_audit' | 'existing_workflow_engine' | 'depth'\n  | 'proxy';                                                                  // @deprecated alias for 'mcp'\n```\n\n### `bl.budgets`\n\nFirst-class budget primitive. The check runs at `govern()` time — exceeding a hard limit returns `decision: 'deny'` with `denial_reason: 'budget'`.\n\n```typescript\nawait bl.budgets.list();                                                     // returns Budget[] (peeled from the list envelope)\nawait bl.budgets.create({ name, scope_type, scope_id?, period, soft_limit_usd?, hard_limit_usd, enabled? });\nawait bl.budgets.get(id);\nawait bl.budgets.status(id);                                                 // current spend + projected hit dates for soft/hard\nawait bl.budgets.workspaceStatus();                                          // status for every enabled budget plus the \"tightest\" budget (least headroom)\nawait bl.budgets.update(id, patch);\nawait bl.budgets.delete(id);\n```\n\n`BudgetScope` enum: `'workspace' | 'agent' | 'tool' | 'user' | 'workflow' | 'run' | 'step'`. `BudgetPeriod` enum: `'per_task' | 'day' | 'week' | 'month'`.\n\n### `bl.insights`\n\nCoverage, risk, drift, anomalies, baselines, and counterfactual reports. These power the console dashboards but are also fine to call directly from automation.\n\n```typescript\nawait bl.insights.coverage();                                                // actors + tools + capture-path attribution\nawait bl.insights.risk();                                                    // decision breakdown, top deniers, high-risk tools\nawait bl.insights.healthSnapshot();                                          // 7-day workspace digest\nawait bl.insights.drift();                                                   // cost change vs prior window + hypothesis hints\nawait bl.insights.coverageTrend(windowDays?);                                // densified per-day governed-vs-uncovered series (default 30)\n\nawait bl.insights.anomalies({ includeDismissed?, limit? });                  // active anomalies for the workspace\nawait bl.insights.recomputeAnomalies(windowDays?);                           // re-detect over the given window\nawait bl.insights.dismissAnomaly(id);                                        // stop surfacing one anomaly\n\nawait bl.insights.observations({ kind?, limit? });                           // workspace observation feed — anomalies, drift, hints, gaps\nawait bl.insights.baselines(windowDays?);                                    // per-(agent, tool) token + cost percentiles\nawait bl.insights.recomputeBaselines(windowDays?);\n\nawait bl.insights.modelChoice(windowDays?);                                  // per-(agent, tool) model usage comparison (≥ 2 models)\nawait bl.insights.modelSubstitution({ from, to, windowDays? });              // counterfactual: cost if every `from` call had used `to`\n\nawait bl.insights.explain(evaluationId);                                     // counterfactual + policies-considered for one evaluation\n```\n\n### `bl.audit`\n\nExport the audit ledger, ingest external events, and inspect coverage gaps.\n\n```typescript\n// Export hot (Postgres) rows only — default, backward-compatible\nconst ndjson = await bl.audit.export({\n  from: new Date(Date.now() - 30 * 86400_000),\n  to: new Date(),\n  kinds: ['evaluation', 'approval', 'action_result'],\n});\n\n// Include archived (GCS cold-storage) rows — BL-OPS-4b\n// Use when your window predates the retention cutoff (default 90 days).\n// Archived rows are prepended to live rows. At the hot/cold boundary global\n// sort order is not guaranteed — re-sort client-side if strict ordering is\n// required.\nconst fullNdjson = await bl.audit.export({\n  from: new Date(Date.now() - 200 * 86400_000),\n  to: new Date(),\n  kinds: ['evaluation'],\n  includeArchived: true,\n});\n\n// Ingest an external event for reconciliation (e.g. a GitHub Actions run)\nconst event = await bl.audit.ingest({\n  source: 'github',\n  source_event_id: 'run-123',\n  event_type: 'workflow_run',\n  resource: 'my-org/my-repo',\n  occurred_at: new Date().toISOString(),\n  payload: { conclusion: 'success' },\n});\n\nawait bl.audit.listEvents({ source: 'github', limit: 50 });  // paginated list\nawait bl.audit.listUncovered({ limit: 25 });                  // events with no matched evaluation\n```\n\nEach line of the exported NDJSON has shape `{ type: 'evaluation' | 'approval' | 'action_result', data: { ... } }`. The window is capped at 365 days server-side; for longer ranges call repeatedly with non-overlapping windows.\n\n### `bl.system`\n\n```typescript\nawait bl.system.mode();    // { mode: 'local' | 'cloud', api_key?: string }    — unauthenticated\nawait bl.system.health();  // { status: 'ok' }                                  — unauthenticated\nawait bl.system.me();      // { auth_mode, user?, api_key?, organisation }     — identify the calling actor\nawait bl.system.quota();   // { plan, used, limit, remaining, ... }            — plan + usage; free tier returns limit, paid tier returns null\n```\n\nUse `bl.system.mode()` to detect whether you're talking to a local CLI-hosted Surface or the cloud one. `me()` resolves the actor for both session-cookie and API-key callers — the response includes the resolved organisation either way.\n\n### `bl.mcp`\n\n`bl.mcp.list/reconnect/rotate` operate on the **local-mode in-memory registry**. `bl.mcp.upstreams.*` operates on the **persistent org-scoped catalogue** the cloud proxy reads from — that's where new upstreams live in production.\n\n```typescript\n// Local registry\nawait bl.mcp.list();                                                          // { servers: McpServerStatus[], config_path: string }\nawait bl.mcp.reconnect(serverName);                                           // { connected, tools, error? }\n\n// Org-scoped upstream catalogue\nawait bl.mcp.upstreams.list();                                                // { upstreams: McpUpstream[] }\nawait bl.mcp.upstreams.get(id);\nawait bl.mcp.upstreams.test(id);                                              // one-shot connection probe\nawait bl.mcp.upstreams.health(id);                                            // health sparkline + uptime estimate\n\n// Rotate credentials for an upstream without losing its row or bindings.\n// static_headers — pass new headers; the stored values are replaced in-place.\nconst result = await bl.mcp.rotate(upstreamId, { headers: { Authorization: 'Bearer new-key' } });\n// → { rotation: 'headers_rotated', upstream_id, message, upstream }\n\n// oauth2 — clears the user's stored token and returns a fresh authorization URL.\n// The caller must redirect the user to authorization_url to complete re-auth.\nconst result = await bl.mcp.rotate(upstreamId);\n// → { rotation: 'oauth_reauth_required', authorization_url, state, expires_at }\n```\n\nManage MCP upstream servers programmatically (status, forced reconnect, credential rotation). Same\nendpoints the console MCP Servers page uses.\n\n`bl.mcp.rotate()` keeps the upstream row and all its agent/tool/policy bindings intact — it is the correct way to swap an API key that changed, or to force a user through OAuth consent again without deleting and recreating the upstream. For `static_headers` upstreams, include `{ headers: { ... } }` in the options; for `oauth2` upstreams the options argument is ignored and the response contains an `authorization_url` the user must visit. OAuth rotation requires a session-authenticated caller — org-scoped API keys will receive a `401 USER_AUTH_REQUIRED`.\n\n### Admin endpoints\n\nEnterprise-audit surfaces exposed at the HTTP layer. No typed SDK method — call them directly with `bl` headers or curl.\n\n```http\nGET /v1/admin/audit/events?action=&actor_user_id=&actor_api_key_id=&from=&to=&limit=&offset=\n```\n\nPrivileged-action log: every operator action against the control plane (key creation, webhook edits, membership changes, etc.). Read-only; the recording side is wired into the mutation routes via `recordAdminAction`. Filters are AND'd. `limit` caps at 500; `offset` for pagination. Returns `{ events: AdminAuditEvent[], total, limit, offset }`.\n\n```http\nGET /v1/admin/access-review\n```\n\nPoint-in-time inventory of every active actor + credential in the workspace, the set an auditor attests to. Returns `{ organisation, members, api_keys, webhooks, mcp_upstreams, github_installations }`. API key entries include the suffix only — never the raw key. Webhooks include URL + event subscription; MCP upstreams include `auth_type`, URL, and `last_pinged_at`.\n\n### Demo endpoints\n\nUsed by the console \"Try it\" buttons and integration test harnesses to populate / wipe a workspace with sentinel demo data (`owner = 'demo'`). Safe to call repeatedly; idempotent on the seed side.\n\n```http\nPOST /v1/demo/seed\n```\n\nIf demo records already exist for this org, returns `{ status: 'already_seeded' }` without re-inserting. On a fresh seed, creates a representative set of agents, tools, policies, bindings, evaluations, cost records, and budgets, and returns `{ agents, tools, policies, bindings, evaluations, cost_records, budgets }` counts.\n\n```http\nPOST /v1/demo/clear\n```\n\nRemoves every record owned by `owner = 'demo'` for the workspace (agents, tools, bindings, evaluations, cost records, budgets). Safe to call with no demo data; returns the counts of what was removed. Use this between integration test runs to keep a workspace clean without dropping the org.\n\n### Error Handling\n\n```typescript\nimport { BlackLake, BlackLakeError } from '@blacklake-systems/surface-sdk';\n\ntry {\n  await bl.govern({ agent: 'unknown', tool: 'unknown' });\n} catch (err) {\n  if (err instanceof BlackLakeError) {\n    console.error(err.status, err.code, err.message);\n    if (err.isRetriable()) {\n      // 5xx / 408 / 429 — safe to back off and retry\n    }\n  }\n}\n```\n\n`BlackLakeError.isRetriable()` returns `true` for HTTP 5xx, `408 Request Timeout`, and `429 Too Many Requests`. 4xx client errors are not retriable — fix the request instead.\n\n## Documentation\n\nFull documentation at [blacklake.systems/docs](https://blacklake.systems/docs).\n","readmeFilename":"README.md"}