{"_id":"@bankplanet9/milkyway-payments","name":"@bankplanet9/milkyway-payments","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@bankplanet9/milkyway-payments","version":"1.0.0","description":"Official TypeScript client for the MilkyWay Payments API (/payments/v1) — partner-facing cross-bank payments.","keywords":["milkyway","payments","planet9","fintech","settlement","keycloak"],"license":"MIT","author":{"name":"Planet9"},"repository":{"type":"git","url":"git+https://github.com/bankplanet9/milkyway-typescript-sdk.git"},"homepage":"https://github.com/bankplanet9/milkyway-typescript-sdk#readme","bugs":{"url":"https://github.com/bankplanet9/milkyway-typescript-sdk/issues"},"type":"module","engines":{"node":">=18"},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","lint":"eslint .","test":"vitest run","test:watch":"vitest"},"dependencies":{"decimal.js":"^10.4.3"},"devDependencies":{"@eslint/js":"^9.18.0","@types/node":"^20.11.0","eslint":"^9.18.0","tsup":"^8.3.5","typescript":"^5.7.3","typescript-eslint":"^8.21.0","vitest":"^2.1.8"},"gitHead":"24f4fd65d9b6d91315a3813fa9d9163b93ee5edb","_id":"@bankplanet9/milkyway-payments@1.0.0","_nodeVersion":"25.6.1","_npmVersion":"11.9.0","dist":{"integrity":"sha512-aZUtTxeI6HlRuKCS+jpkQ0Pnheb9qOXR4zKoSpiGewZ63q9wrJjRnYL6Co1dR63f66yBERhV102dB9PRGvukog==","shasum":"8151cc9113c44f19918fc70bfbfc4454f295a5ee","tarball":"https://registry.npmjs.org/@bankplanet9/milkyway-payments/-/milkyway-payments-1.0.0.tgz","fileCount":9,"unpackedSize":164295,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDbJVELkPpLpOOzKawuAm0FlHnVkxXlAh5Qq3M1E0SXrAiEA7CB7vDGtT0RP7U+SF9ikhc3KAJBJqzAkyIoyXS7WSVc="}]},"_npmUser":{"name":"bankplanet9","email":"bekhruz@planet9.ae"},"directories":{},"maintainers":[{"name":"bankplanet9","email":"bekhruz@planet9.ae"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/milkyway-payments_1.0.0_1782133476172_0.4191588723575377"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-22T13:04:36.013Z","1.0.0":"2026-06-22T13:04:36.304Z","modified":"2026-06-22T13:04:36.510Z"},"maintainers":[{"name":"bankplanet9","email":"bekhruz@planet9.ae"}],"description":"Official TypeScript client for the MilkyWay Payments API (/payments/v1) — partner-facing cross-bank payments.","homepage":"https://github.com/bankplanet9/milkyway-typescript-sdk#readme","keywords":["milkyway","payments","planet9","fintech","settlement","keycloak"],"repository":{"type":"git","url":"git+https://github.com/bankplanet9/milkyway-typescript-sdk.git"},"author":{"name":"Planet9"},"bugs":{"url":"https://github.com/bankplanet9/milkyway-typescript-sdk/issues"},"license":"MIT","readme":"# MilkyWay Payments SDK for TypeScript\n\n[![npm](https://img.shields.io/npm/v/@bankplanet9/milkyway-payments.svg?logo=npm)](https://www.npmjs.com/package/@bankplanet9/milkyway-payments)\n[![CI](https://github.com/bankplanet9/milkyway-typescript-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/bankplanet9/milkyway-typescript-sdk/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nOfficial TypeScript/JavaScript client for the **MilkyWay Payments API**\n(`/payments/v1`) — the partner-facing API that banks use to initiate, quote,\ntrack, and cancel cross-bank payments.\n\nBatteries included:\n\n- **Keycloak client-credentials auth** with in-memory token caching, automatic\n  refresh, single-flight acquisition, and a one-shot refresh-and-replay on `401`.\n- **Retries**: exponential backoff with full jitter on transient failures\n  (5xx, 408, network); deterministic errors (400/401/402/404) are never retried;\n  a `pay` without an `Idempotency-Key` is never auto-retried.\n- **Exact-decimal money** via [decimal.js](https://github.com/MikeMcl/decimal.js/)\n  — monetary fields never round-trip through a lossy float64.\n- **Typed models & errors** — status is an `enum`, and each HTTP error maps to a\n  specific error class carrying `statusCode` + the server message.\n- **Zero runtime deps beyond `decimal.js`** — uses the built-in `fetch`\n  (Node 18+); a custom `fetch` is injectable for testing.\n- Ships **ESM + CommonJS + type declarations**. Targets **Node 18+**.\n\n## Install\n\n```bash\nnpm install @bankplanet9/milkyway-payments\n```\n\n## Quick start\n\n```ts\nimport { MilkywayPaymentsClient, TransactionStatus } from \"@bankplanet9/milkyway-payments\";\nimport { randomUUID } from \"node:crypto\";\n\nconst client = new MilkywayPaymentsClient({\n  baseUrl: \"https://milkyway.stage.planet9.ae\",\n  tokenUrl: \"https://keycloak.ac8o.planet9.ae/realms/planet9-stage/protocol/openid-connect/token\",\n  clientId: \"your-client-id\", // issued to your institution\n  clientSecret: \"your-client-secret\",\n});\n\n// 1. Is the recipient bank's service online?\nawait client.healthcheck(\"bank-beta\", \"card-payout\");\n\n// 2. Quote the payment (FX markup + commission applied here).\nconst quote = await client.precheck({\n  third_party_id_debit: \"bank-beta\",\n  service_id: \"card-payout\",\n  recipient_id: \"recipient-9999\",\n  amount_credit: \"100.00\", // string | number | Decimal — kept exact\n  currency_credit: \"USD\",\n});\nconsole.log(`Rate ${quote.rate}, debit ${quote.amount_debit} ${quote.currency_debit}, commission ${quote.commission}`);\n\n// 3. Initiate the payment. Pass an idempotencyKey so retries are safe.\nconst transactionId = await client.pay(\n  {\n    third_party_id_debit: \"bank-beta\",\n    service_id: \"card-payout\",\n    sender_id: \"sender-0001\",\n    recipient_id: \"recipient-9999\",\n    amount_credit: \"100.00\",\n    currency_credit: \"USD\",\n    data: { passport: \"AA1234567\" },\n  },\n  { idempotencyKey: randomUUID() },\n);\n\n// 4. Poll until the payment reaches a terminal status.\nconst result = await client.waitForCompletion(transactionId);\nconsole.log(`Final status: ${TransactionStatus[result.status]}`);\n```\n\n`amount_credit`, `amount_debit`, `rate`, and `commission` come back as\n[`Decimal`](https://github.com/MikeMcl/decimal.js/) instances (re-exported from\nthis package as `Decimal`). Use `.toString()`, `.toFixed()`, `.plus()`, etc.;\nnever coerce them through `Number()` if you care about precision.\n\n## Money precision\n\nThe Payments API encodes money as JSON **numbers**. `JSON.parse` would coerce\nthose to IEEE-754 float64 and silently lose precision on large amounts or rates\nwith many significant digits. This SDK avoids that entirely: it pre-scans the\nraw response text for the known money keys, extracts their exact digit strings,\nand reconstructs a `Decimal` from the original digits — never from a float\nround-trip. On the way out, `Decimal` request amounts are written back as bare\nJSON numbers (not strings), so the wire format matches the API contract exactly.\n\n## Cancellation & timeouts\n\nEvery method accepts an `AbortSignal` so you can cancel in-flight requests:\n\n```ts\nconst controller = new AbortController();\nconst p = client.precheck(req, { signal: controller.signal });\ncontroller.abort(); // rejects p\n```\n\nA per-attempt timeout (`requestTimeoutMs`, default 30s) is enforced internally\nvia its own `AbortSignal`; a timed-out attempt is treated as transient and\nretried (subject to the retry rules below).\n\n## Errors\n\nAll API errors are subclasses of `MilkywayApiError` (carrying `statusCode` and\nthe server's message in `.message`, plus the raw `.responseBody`):\n\n| HTTP | Error class | Meaning |\n| --- | --- | --- |\n| 400 | `MilkywayValidationError` | Bad request (invalid amount, missing field, unresolvable FX rate). |\n| 401 | `MilkywayAuthError` | Token missing/invalid (also thrown if token acquisition fails). |\n| 402 | `MilkywayExposureBlockedError` | Payment would breach a block-action exposure limit. |\n| 404 | `MilkywayNotFoundError` | Transaction not found or not owned by your institution. |\n| 5xx | `MilkywayServiceUnavailableError` | API or downstream recipient unavailable (retried automatically first). |\n\n```ts\nimport { MilkywayExposureBlockedError } from \"@bankplanet9/milkyway-payments\";\n\ntry {\n  await client.pay(req, { idempotencyKey: key });\n} catch (err) {\n  if (err instanceof MilkywayExposureBlockedError) {\n    // handle the exposure block specifically\n  }\n  throw err;\n}\n```\n\n## Retries & idempotency\n\nTransient failures (HTTP 5xx, 408, network/timeout) are retried automatically\nwith exponential backoff + full jitter, tunable via `maxRetries` (default 3) and\n`retryBaseDelayMs` (default 500). Deterministic errors (400/401/402/404) are\nnever retried.\n\n**`pay` is only auto-retried when you supply an `idempotencyKey`** — without one,\na retry could create a duplicate payment, so the SDK sends it exactly once. With\na key, the call is safe to retry and the key is forwarded as the\n`Idempotency-Key` header.\n\n## Configuration\n\n| Option | Default | Purpose |\n| --- | --- | --- |\n| `baseUrl` | — (required) | Payments API base URL. |\n| `tokenUrl` | — (required) | Keycloak token endpoint. |\n| `clientId` / `clientSecret` | — (required) | Your institution's credentials. |\n| `scope` | none | Optional OAuth scope. |\n| `tokenRefreshSkewMs` | 30000 | Refresh this long before token expiry. |\n| `requestTimeoutMs` | 30000 | Per-attempt request timeout. |\n| `maxRetries` | 3 | Max transient-failure retries. |\n| `retryBaseDelayMs` | 500 | Base delay for exponential backoff. |\n| `fetch` | global `fetch` | Injectable fetch implementation (for testing). |\n\n`waitForCompletion` accepts its own `PollOptions`: `initialDelayMs` (1000),\n`maxDelayMs` (30000), `backoffMultiplier` (2.0), `timeoutMs` (300000). When the\npoll budget is exhausted, the last observed (non-terminal) status is returned.\n\n## Building from source\n\n```bash\nnpm install\nnpm run lint\nnpm run typecheck\nnpm test\nnpm run build   # emits dist/ (ESM + CJS + .d.ts)\n```\n\n## Releasing\n\nReleases are **fully automated** by\n[semantic-release](https://semantic-release.gitbook.io/) on every push to `main`:\n\n1. Conventional commits are analysed (`feat:` → minor, `fix:`/`perf:` → patch,\n   `!` / `BREAKING CHANGE` → major). No releasable commits → no release.\n2. `@semantic-release/npm` writes the computed version into `package.json`.\n3. The package is built and published to **npm with provenance**\n   (`npm publish --provenance --access public`) via GitHub OIDC — no long-lived\n   token stored.\n4. A GitHub release + `vX.Y.Z` tag is created with generated notes.\n\nThe publish job is **guarded** behind the repo variable\n`PUBLISH_ENABLED == 'true'`, so until the registry side is configured, pushes to\n`main` build and test but never fail on a missing publish setup.\n\n### One-time npm setup (maintainers)\n\n1. **Create the org/scope.** On npmjs.com, create the `@bankplanet9` organization\n   (the package is scoped `@bankplanet9/milkyway-payments`).\n2. **Configure Trusted Publishing (preferred — no token).** On npmjs.com →\n   the package's **Settings → Trusted Publisher**, add a GitHub Actions publisher\n   for owner `bankplanet9`, repository `milkyway-typescript-sdk`, workflow\n   `ci.yml`. The workflow already requests `id-token: write` and publishes with\n   `--provenance`, so no secret is needed.\n   - *Note:* the first publish of a brand-new package name may need to be done\n     once manually (`npm publish --access public`) to create the package before a\n     trusted-publisher policy can be attached, depending on npm's current rules.\n3. **Or fall back to a token.** If you prefer a classic token, create an\n   **Automation** access token on npmjs.com and add it as the repo secret\n   `NPM_TOKEN`; the workflow wires it as `NODE_AUTH_TOKEN`.\n4. **Enable releases.** Set the repository **variable** `PUBLISH_ENABLED` to\n   `true` (Settings → Secrets and variables → Actions → Variables).\n\n## License\n\nMIT — see [LICENSE](LICENSE). Copyright (c) 2026 Planet9.\n","readmeFilename":"README.md","_rev":"1-724e10758214732aa344a52c5aab6205"}