{"_id":"@cubiczan/resilience","name":"@cubiczan/resilience","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@cubiczan/resilience","version":"0.1.0","description":"Dependency-light resilience primitives: safeFetch (timeout + retry/backoff + SSRF allowlist), fail-closed requireAuth with in-memory rate limiting, and composable withTimeout/retry.","type":"module","license":"MIT","author":{"name":"Shyam Desigan","email":"sam@cubiczan.com"},"engines":{"node":">=18"},"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"sideEffects":false,"scripts":{"build":"tsc","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","prepublishOnly":"npm run build"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/icohangar-ops/cubiczan-resilience.git","directory":"typescript"},"homepage":"https://github.com/icohangar-ops/cubiczan-resilience#readme","bugs":{"url":"https://github.com/icohangar-ops/cubiczan-resilience/issues"},"peerDependencies":{"zod":"^3.0.0 || ^4.0.0"},"peerDependenciesMeta":{"zod":{"optional":true}},"devDependencies":{"@types/node":"^20.19.43","typescript":"^5.4.0","vitest":"^2.0.0","zod":"^3.23.0"},"keywords":["resilience","retry","backoff","timeout","fetch","ssrf","rate-limit","auth","fail-closed"],"gitHead":"d9d367eeb1f6711f9afd24bb4dcc6216677c3de2","_id":"@cubiczan/resilience@0.1.0","_nodeVersion":"26.0.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-jYUt5Hc+C9qR75U5wA8/vAAi34oNn9OIAsGBRZYWlWw4a4raLFgn4a3IniHnFZka7Mx9MboMiTTViZJaaKUJoQ==","shasum":"b4f6359d87e71b2334b08b0f01d2ff15fd47701d","tarball":"https://registry.npmjs.org/@cubiczan/resilience/-/resilience-0.1.0.tgz","fileCount":43,"unpackedSize":88844,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDcVfLS0RBF0y2H5vxYpU7yl5U8V0QYAvJ0x96Oh1aibgIhALgUe8afz6WzHOn+ew5hjavJ+dvh3YC5C+o09lKVKHbd"}]},"_npmUser":{"name":"cubiczan","email":"icohangar@gmail.com"},"directories":{},"maintainers":[{"name":"cubiczan","email":"icohangar@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/resilience_0.1.0_1787368219417_0.9583437039585514"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-22T03:10:19.092Z","0.1.0":"2026-08-22T03:10:19.602Z","modified":"2026-08-22T03:10:19.826Z"},"maintainers":[{"name":"cubiczan","email":"icohangar@gmail.com"}],"description":"Dependency-light resilience primitives: safeFetch (timeout + retry/backoff + SSRF allowlist), fail-closed requireAuth with in-memory rate limiting, and composable withTimeout/retry.","homepage":"https://github.com/icohangar-ops/cubiczan-resilience#readme","keywords":["resilience","retry","backoff","timeout","fetch","ssrf","rate-limit","auth","fail-closed"],"repository":{"type":"git","url":"git+https://github.com/icohangar-ops/cubiczan-resilience.git","directory":"typescript"},"author":{"name":"Shyam Desigan","email":"sam@cubiczan.com"},"bugs":{"url":"https://github.com/icohangar-ops/cubiczan-resilience/issues"},"license":"MIT","readme":"# @cubiczan/resilience\n\nDependency-light, ESM-first resilience primitives for TypeScript/Node.\n\nLifted and generalized from proven patterns in the portfolio:\n\n- timeout-via-`Promise.race`/`AbortController` (agent-conductor)\n- fail-closed bearer-token auth (AgentPay `require-auth.ts`)\n- Zod-validated request boundaries (swarmfi-preps)\n\n**Zero runtime dependencies.** `zod` is an *optional* peer used only by the\nvalidation helper.\n\n```bash\nnpm install @cubiczan/resilience\n# optional, only if you use validateBoundary():\nnpm install zod\n```\n\nRequires Node 18+ (global `fetch` / `AbortController`). Targets ES2022, ships\nESM + `.d.ts`.\n\n## Exports\n\n| Export | Purpose |\n|---|---|\n| `safeFetch(url, opts)` | fetch with per-attempt timeout, retry+backoff+jitter on 429/5xx & network errors, fail-fast on 4xx, optional SSRF allowlist |\n| `requireAuth(req, opts)` | fail-closed bearer check + sliding-window rate limit (generic predicate) |\n| `requireAuthResponse(req, opts)` | Next.js-style helper — returns a `Response` to send, or `null` if authorized |\n| `withTimeout(promise, ms)` | bound any promise with a typed timeout |\n| `retry(fn, opts)` | exponential backoff + full jitter, composable |\n| `SlidingWindowRateLimiter` | in-memory sliding-window limiter |\n| `validateBoundary(schema, input)` | validate untrusted input via a Zod-compatible schema |\n| `AuditLedger` / `verifyLedger` | signed, append-only JSONL audit ledger with HMAC-SHA256 signature chaining (see the [Audit Ledger](../README.md#audit-ledger) section) |\n| `ResilienceError` / `isResilienceError` | typed error with `kind`, `attempts`, `status` |\n\n---\n\n## `safeFetch`\n\n```ts\nimport { safeFetch, isResilienceError } from \"@cubiczan/resilience\";\n\ntry {\n  const res = await safeFetch(\"https://api.example.com/v1/orders\", {\n    method: \"POST\",\n    headers: { \"content-type\": \"application/json\" },\n    body: JSON.stringify({ sku: \"abc\" }),\n    timeoutMs: 5_000,     // per attempt (AbortController)\n    maxAttempts: 3,       // default 3\n    baseDelayMs: 250,     // default 250 (exponential w/ full jitter)\n    allowlist: [\"api.example.com\"], // optional SSRF guard\n  });\n  const data = await res.json();\n} catch (err) {\n  if (isResilienceError(err)) {\n    // err.kind: \"timeout\" | \"network\" | \"http\" | \"ssrf\" | \"exhausted\" | \"aborted\"\n    console.error(err.kind, err.status, err.attempts);\n  }\n}\n```\n\nBehavior:\n\n- **Per-attempt timeout** via a fresh `AbortController` each try (also linked to\n  a caller-supplied `signal`).\n- **Retries** on `408/425/429/500/502/503/504` and network errors, with\n  exponential backoff + full jitter.\n- **Fail-fast** on other 4xx — a `404`/`400` is returned to you as a `Response`,\n  not retried.\n- **SSRF allowlist** runs once before any I/O. Pass an array of hostnames or a\n  `(url: URL) => boolean` hook; non-allowlisted hosts throw `kind: \"ssrf\"`.\n- On exhausting retries it throws a typed `ResilienceError` (the last retryable\n  status is preserved on `.status`).\n\n---\n\n## `requireAuth` / `requireAuthResponse`\n\nFail-closed: if the expected token is **unset**, the request is **refused**\n(503) — it never degrades to open. A missing/mismatched token is `401`.\n\nGeneric predicate:\n\n```ts\nimport { requireAuth, SlidingWindowRateLimiter } from \"@cubiczan/resilience\";\n\nconst limiter = new SlidingWindowRateLimiter({ limit: 60, windowMs: 60_000 });\n\nexport async function handler(req: Request) {\n  const auth = requireAuth(req, {\n    token: process.env.API_TOKEN, // undefined => 503, not allowed\n    limiter,\n    keyFor: (req) => req.headers.get(\"x-forwarded-for\") ?? \"anon\",\n  });\n  if (!auth.ok) {\n    return new Response(JSON.stringify({ error: auth.reason }), {\n      status: auth.status, // 401 | 503 | 429\n    });\n  }\n  // ...authorized; auth.token available\n}\n```\n\nNext.js-style helper (mirrors AgentPay's `requireAuth(req): Response | null`):\n\n```ts\nimport { requireAuthResponse } from \"@cubiczan/resilience\";\n\nexport async function POST(req: Request) {\n  const denied = requireAuthResponse(req, {\n    token: process.env.API_TOKEN,\n    rateLimit: { limit: 10, windowMs: 60_000 }, // limiter auto-created & reused\n  });\n  if (denied) return denied; // 401/503/429 with JSON body (+ retry-after on 429)\n\n  // ...do the money-moving work\n  return Response.json({ ok: true });\n}\n```\n\n---\n\n## `withTimeout` & `retry` (composable primitives)\n\n```ts\nimport { withTimeout, retry } from \"@cubiczan/resilience\";\n\n// Bound any promise:\nconst rows = await withTimeout(db.query(\"SELECT ...\"), 2_000, \"db-query\");\n\n// Retry any async fn with backoff + jitter, fail-fast on non-retryable errors:\nconst result = await retry(\n  async (attempt) => callFlakyApi(),\n  {\n    maxAttempts: 4,\n    baseDelayMs: 200,\n    shouldRetry: (err) => !(err instanceof FatalError),\n    onRetry: ({ attempt, delayMs }) => console.warn(`retry ${attempt} in ${delayMs}ms`),\n  },\n);\n```\n\n---\n\n## `validateBoundary` (optional, needs `zod`)\n\n```ts\nimport { z } from \"zod\";\nimport { validateBoundary } from \"@cubiczan/resilience\";\n\nconst Payment = z.object({ amount: z.number().positive(), to: z.string() });\n\n// Throws a ResilienceError (status 400) on invalid input:\nconst payment = validateBoundary(Payment, await req.json(), \"payment\");\n```\n\nAny object exposing a Zod-style `safeParse` works — no hard dependency on `zod`.\n\n---\n\n## Scripts\n\n```bash\nnpm run build      # tsc -> dist/ (ESM + .d.ts)\nnpm run typecheck  # tsc --noEmit\nnpm test           # vitest run\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-13e3708d05e0a9d57445595cdcaead71"}