{"_id":"@alien-lobster-buffet/media-forge-client","name":"@alien-lobster-buffet/media-forge-client","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@alien-lobster-buffet/media-forge-client","version":"0.1.0","description":"Typed tenant SDK for the Media Forge media-generation API — submit jobs, poll to completion, verify webhooks.","keywords":["media-forge","media-generation","tts","image-generation","sdk","client"],"license":"MIT","author":{"name":"Cole Reed","email":"alienlobsterbuffet.dev@gmail.com"},"type":"module","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"main":"./dist/index.js","types":"./dist/index.d.ts","engines":{"node":">=18"},"publishConfig":{"access":"public"},"scripts":{"build":"tsdown","test":"bunx --bun vitest run","typecheck":"bunx tsc --noEmit","lint":"biome check src/","prepublishOnly":"bun run build"},"devDependencies":{"@media-forge/shared":"workspace:*","@media-forge/typescript-config":"workspace:*","tsdown":"^0.15.6"},"gitHead":"a21c8a35ea51b30c8be0a4499b33afbb879002cd","_id":"@alien-lobster-buffet/media-forge-client@0.1.0","_nodeVersion":"23.9.0","_npmVersion":"11.11.0","dist":{"integrity":"sha512-/WoTAfB3S66OrEFWtGn90/fX7ahoSQq7Mb0PSZFDUfc6g7q6/VZs9rZSPcEtG5ucWFIF230txgvjUF7S9+vlJw==","shasum":"0ff2cf8f5f98030c296f2561092f7d84609393f0","tarball":"https://registry.npmjs.org/@alien-lobster-buffet/media-forge-client/-/media-forge-client-0.1.0.tgz","fileCount":4,"unpackedSize":22825,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDVl0Y7/iws7tQ7z/I9o7t6QfsvO0eEdcIu9f9/i2Ry8AiEAjdTQS9913/QjJtL1LvMnHr4zHhD9/QJD0iInzcqnxFU="}]},"_npmUser":{"name":"ichabodcole","email":"ichabodcole@gmail.com"},"directories":{},"maintainers":[{"name":"ichabodcole","email":"ichabodcole@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/media-forge-client_0.1.0_1785704250603_0.08319549833426221"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-02T20:57:30.380Z","0.1.0":"2026-08-02T20:57:30.756Z","modified":"2026-08-02T20:57:30.968Z"},"maintainers":[{"name":"ichabodcole","email":"ichabodcole@gmail.com"}],"description":"Typed tenant SDK for the Media Forge media-generation API — submit jobs, poll to completion, verify webhooks.","keywords":["media-forge","media-generation","tts","image-generation","sdk","client"],"author":{"name":"Cole Reed","email":"alienlobsterbuffet.dev@gmail.com"},"license":"MIT","readme":"# @alien-lobster-buffet/media-forge-client\n\nTyped tenant SDK for the [Media Forge](https://github.com/ichabodcole/media-forge) media-generation API. Submit TTS and image-generation jobs, poll them to completion, and verify delivery webhooks — with the full wire contract typed.\n\nThis is the **tenant/consumer** surface (bearer-authenticated). The admin/session surface stays in the Media Forge admin app and is intentionally out of scope here.\n\n## Install\n\n```bash\nnpm install @alien-lobster-buffet/media-forge-client\n# or: bun add / pnpm add / yarn add\n```\n\nNode 18+ (uses global `fetch` and `node:crypto`). ESM-only.\n\n## Quick start\n\n```ts\nimport { createMediaForgeClient } from \"@alien-lobster-buffet/media-forge-client\";\n\nconst mf = createMediaForgeClient({\n  baseUrl: \"https://api.mediaforge.dev\",\n  apiKey: process.env.MEDIA_FORGE_API_KEY!,\n  tenant: \"acme\", // your tenant slug (asserted against the key server-side)\n});\n\n// Submit a job…\nconst { serviceJobId } = await mf.submitJob({\n  type: \"tts\",\n  params: {\n    sourceText: \"Hello from Media Forge.\",\n    voiceId: \"your-voice-id\",\n    voiceSettings: { stability: 0.5, similarityBoost: 0.75 },\n  },\n});\n\n// …then poll it to completion.\nconst outcome = await mf.waitForJob(serviceJobId, { timeoutMs: 120_000 });\n\nif (outcome.status === \"completed\") {\n  console.log(outcome.job.outputs);\n} else if (outcome.status === \"timed-out\") {\n  // Not an error — the job may still finish. Re-poll or wait on the webhook.\n}\n```\n\n## Client API\n\n`createMediaForgeClient(config)` returns:\n\n| Method                        | Endpoint                     | Notes                                                       |\n| ----------------------------- | ---------------------------- | ----------------------------------------------------------- |\n| `submitJob({ type, params })` | `POST /jobs`                 | Discriminated on `type` (`\"tts\"` \\| `\"image-gen\"`).         |\n| `getJob(id)`                  | `GET /jobs/:id`              | Current status, outputs, and cost surface.                  |\n| `cancelJob(id)`               | `POST /jobs/:id/cancel`      | Cooperative cancel of a `QUEUED`/`RUNNING` job.             |\n| `refreshUrl(id)`              | `POST /jobs/:id/refresh-url` | Mint fresh presigned URLs for a `COMPLETED` job's outputs.  |\n| `waitForJob(id, options?)`    | polls `GET /jobs/:id`        | Backoff + jitter to a terminal state or timeout. See below. |\n\n### `waitForJob`\n\nPolls until the job reaches a terminal state or the deadline elapses. Returns a typed outcome:\n\n```ts\ntype JobOutcome =\n  | { status: \"completed\"; job: JobResponse }\n  | { status: \"failed\"; job: JobResponse }\n  | { status: \"canceled\"; job: JobResponse }\n  | { status: \"timed-out\"; job: JobResponse }; // last snapshot seen\n```\n\nOptions: `pollIntervalMs` (default 1000), `maxPollIntervalMs` (default 5000), `timeoutMs` (default 300000), `signal` (an `AbortSignal` — aborting rejects with `signal.reason`), and `onPoll(job)` for progress.\n\n`timed-out` is a **normal return**, not a thrown error: the job may still finish server-side, so re-poll or rely on the webhook.\n\n## Webhooks\n\nMedia Forge signs each delivery Stripe-style:\n\n```\nX-Signature = hex HMAC-SHA256( \"<X-Timestamp>.<raw body>\", <tenant webhook secret> )\n```\n\nVerify with the **raw** request body (re-serialized JSON will not match byte-for-byte):\n\n```ts\nimport { verifyAndParseWebhook } from \"@alien-lobster-buffet/media-forge-client\";\n\n// In your webhook handler (raw body as a string):\nconst event = verifyAndParseWebhook({\n  payload: rawBody,\n  signature: req.headers[\"x-signature\"],\n  timestamp: req.headers[\"x-timestamp\"],\n  secret: process.env.MEDIA_FORGE_WEBHOOK_SECRET!,\n}); // throws MediaForgeError on an invalid/stale signature\n\nswitch (event.type) {\n  case \"tts.completed\":\n  case \"image-gen.completed\":\n    // event.outputs — each with presignedUrl + expiresAt\n    break;\n  case \"tts.failed\":\n  case \"image-gen.failed\":\n    // event.error.code / event.error.message\n    break;\n  case \"job.cost.finalized\":\n    // event.costMicrosUsd / event.costSource\n    break;\n}\n```\n\n- `verifyWebhook(options)` → `boolean` — constant-time signature check plus timestamp freshness (`toleranceSeconds`, default 300).\n- `parseWebhook(payload)` → typed `WebhookEnvelope` (does **not** verify).\n- `verifyAndParseWebhook(options)` → verify then parse; throws `MediaForgeError` (code `WEBHOOK_SIGNATURE_INVALID`) on failure.\n\n## Errors\n\nAny non-2xx response throws `MediaForgeError` with `status`, `code`, and `details` mirrored from the API's error envelope:\n\n```ts\nimport { MediaForgeError } from \"@alien-lobster-buffet/media-forge-client\";\n\ntry {\n  await mf.submitJob(/* … */);\n} catch (err) {\n  if (err instanceof MediaForgeError && err.code === \"INVALID_PARAMS\") {\n    // …\n  }\n}\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-b3c875944726e3c4f705b3052400fe3d"}