{"_id":"@astermd-hq/vrio-client","name":"@astermd-hq/vrio-client","dist-tags":{"latest":"0.0.1"},"versions":{"0.0.1":{"name":"@astermd-hq/vrio-client","version":"0.0.1","description":"Unofficial Node.js client for the VRIO commerce API: campaigns, customers, offers, carts, discounts, orders and routes, with opt-in redacted request logging.","license":"MIT","author":{"name":"AsterMD","email":"admin@astermd.com"},"homepage":"https://github.com/astermd/npm-vrio-client#readme","repository":{"type":"git","url":"git+https://github.com/astermd/npm-vrio-client.git"},"bugs":{"url":"https://github.com/astermd/npm-vrio-client/issues","email":"admin@astermd.com"},"keywords":["vrio","vrio-api","api-client","rest-client","ecommerce","orders","payments","checkout"],"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"sideEffects":false,"engines":{"node":">=22"},"scripts":{"lint":"eslint .","lint:fix":"eslint . --fix","format":"prettier --write .","format:check":"prettier --check .","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","build":"tsup","test:dist":"node scripts/check-dist-interop.mjs","gate":"npm run lint && npm run typecheck && npm run test && npm run build && npm run test:dist && npx --yes publint"},"devDependencies":{"@eslint/js":"^9.36.0","@types/node":"^22.18.0","eslint":"^9.36.0","eslint-config-prettier":"^10.1.0","prettier":"^3.6.0","tsup":"^8.5.0","typescript":"^5.9.0","typescript-eslint":"^8.44.0","vitest":"^3.2.0"},"_id":"@astermd-hq/vrio-client@0.0.1","gitHead":"28c549bb801ab010382d6c896473c32627fead04","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-92e3pRFuXwOuUv6rZ7qYXYALEDrnAnUJl+mNCqdXYqZgj9vhm+7b0F3cn3bt0ZnNQKj3KkupUHOTQQ9eze7pbg==","shasum":"657659c819fbbff0dcedd9d66ebd667dda99bd87","tarball":"https://registry.npmjs.org/@astermd-hq/vrio-client/-/vrio-client-0.0.1.tgz","fileCount":9,"unpackedSize":797675,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@astermd-hq%2fvrio-client@0.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCuZbouIFyMbMG/U0yfLx4vW6Wd5ZN3zFCSQaaHLsR9NQIhAOIqIH2Kg74J19CcR5fI5EPg8Hwkw2OsDPfLe7MivjMA"}]},"_npmUser":{"name":"astermd","email":"admin@astermd.com"},"directories":{},"maintainers":[{"name":"astermd","email":"admin@astermd.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/vrio-client_0.0.1_1789058236759_0.006180515349083926"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-10T16:37:16.381Z","0.0.1":"2026-09-10T16:37:16.901Z","modified":"2026-09-10T16:37:17.486Z"},"maintainers":[{"name":"astermd","email":"admin@astermd.com"}],"description":"Unofficial Node.js client for the VRIO commerce API: campaigns, customers, offers, carts, discounts, orders and routes, with opt-in redacted request logging.","homepage":"https://github.com/astermd/npm-vrio-client#readme","keywords":["vrio","vrio-api","api-client","rest-client","ecommerce","orders","payments","checkout"],"repository":{"type":"git","url":"git+https://github.com/astermd/npm-vrio-client.git"},"author":{"name":"AsterMD","email":"admin@astermd.com"},"bugs":{"url":"https://github.com/astermd/npm-vrio-client/issues","email":"admin@astermd.com"},"license":"MIT","readme":"# @astermd-hq/vrio-client\n\nA small, dependency-free Node.js client for the VRIO commerce API — campaigns,\ncustomers, offers, discounts, carts, orders and routes — with opt-in request\nlogging that is redacted by default.\n\n> **Unofficial.** This is an independent client library. It is not the official\n> VRIO SDK and is not affiliated with, endorsed by or supported by VRIO.\n> \"VRIO\" and related marks belong to their owner, <https://www.vrio.com/>. The\n> name is used here only to identify the API this library talks to. See\n> [LICENSE](LICENSE) for the full notice.\n\nAPI reference: **<https://docs.vrio.com/reference/vrio-api-overview>**\n\n## Requirements\n\n- Node.js **22 minimum**, **24 supported**\n- No runtime dependencies\n- **Server-side only.** The package is given an API key, so it must never run\n  in a browser or in any bundle shipped to one.\n\nCI runs the full gate on 22 and 24.\n\n## Installation\n\n```bash\nnpm install @astermd-hq/vrio-client\n```\n\n## Quick start\n\n```ts\nimport { API, isEnvelope } from '@astermd-hq/vrio-client';\n\nconst api = new API(apiKey); // host defaults to api.vrio.app\n\nconst orders = await api.searchOrder({ with: 'items' }).getInObject();\n\nif (isEnvelope(orders.response) && orders.response.success) {\n  // `data` is deliberately `unknown` — this package invents no per-endpoint\n  // response shapes. Cast it to whatever your integration expects.\n  const list = orders.response.data as ReadonlyArray<Record<string, unknown>>;\n  for (const order of list) {\n    // ...\n  }\n}\n```\n\n`orders.response` is typed `Envelope | TransportFailure | WithHeader<Envelope>` —\n`isEnvelope()` narrows it before `.success` and `.data` are readable. See\n[Reading a response](#reading-a-response) for `isTransportFailure()`, the\ncounterpart guard for a request that never reached the provider.\n\nThe API key is a **required argument**. This package ships no default\ncredentials and reads none from the environment.\n\n## Configuration\n\n```ts\nconst api = new API(apiKey, {\n  host: 'api.vrio.app',\n  basePath: '',\n  timeout: 30,\n  connectTimeout: 10,\n  debug: false,\n  debugRedact: true,\n  // debugFile: '/var/log/vrio/client.log', // required when debug is on and no debugSink is given\n  debugRetentionDays: 7,\n  debugTimezone: 'UTC',\n  // debugSink: (entry) => { /* ... */ },   // replaces the file sink entirely\n});\n```\n\n| Option               | Type     | Default        | Purpose                                                                                              |\n| -------------------- | -------- | -------------- | ---------------------------------------------------------------------------------------------------- |\n| `host`               | string   | `api.vrio.app` | **Bare hostname only.** A scheme, path, query or space is rejected.                                  |\n| `basePath`           | string   | `''`           | Optional path prefix below the host, e.g. `v1`.                                                      |\n| `timeout`            | number   | `30`           | Transfer timeout in seconds.                                                                         |\n| `connectTimeout`     | number   | `10`           | Connection timeout in seconds.                                                                       |\n| `debug`              | boolean  | `false`        | Master switch for request/response logging.                                                          |\n| `debugRedact`        | boolean  | `true`         | Mask credentials and sensitive fields in log entries.                                                |\n| `debugFile`          | string   | —              | Base path for the built-in dated file sink. Required when `debug` is on and no `debugSink` is given. |\n| `debugRetentionDays` | number   | `7`            | Days of log history to keep. `0` keeps everything.                                                   |\n| `debugTimezone`      | string   | `UTC`          | IANA timezone for log timestamps and dated filenames.                                                |\n| `debugSink`          | function | —              | Replaces the file sink entirely.                                                                     |\n\n`fetch` settles when the response headers arrive, so `connectTimeout` bounds\nthat phase and `timeout` bounds the whole exchange including the body read.\n\n### Choosing an environment\n\nPass whichever host VRIO issued you. Production defaults to `api.vrio.app`; if\nyour account has a separate test host, supply it the same way:\n\n```ts\nconst test = new API(testKey, { host: testHostFromYourVrioAccount });\n```\n\nFull URLs are rejected on purpose — the scheme is always HTTPS and path\nassembly stays inside the client:\n\n```ts\nnew API(apiKey, { host: 'https://api.vrio.app' }); // throws VrioError\nnew API(apiKey, { host: 'api.vrio.app/v1' }); // throws VrioError\nnew API(apiKey, { host: 'api.vrio.app', basePath: 'v1' }); // correct\n```\n\n## Reading a response\n\nEvery resource method returns a lazy `Call`. Nothing is sent until you read the\nresult. Three accessors read it:\n\n```ts\nawait api.searchOrder().get(); // envelope as a JSON string\nawait api.searchOrder().getInObject(); // envelope decoded to an object\nawait api.searchOrder().getInArray(); // alias of getInObject()\n```\n\n`getInArray()` returns exactly the same value as `getInObject()`, because a\ndecoded JSON object is an object in Node. It is kept so examples transfer from\nthe PHP client unchanged.\n\nA `Call` sends at most once, so reading it twice does not issue two requests.\n\n### Awaiting without an accessor\n\nAwaiting the `Call` itself sends the request and resolves to what\n`getInObject()` returns:\n\n```ts\nawait api.addOrder({ connection_id: 'con_1', email: 'buyer@example.test' });\n```\n\nPass `true` to an accessor to also receive the request URL and payload:\n\n```ts\nawait api.searchOrder({ with: 'items' }).getInObject(true);\n```\n\n```ts\nconst shape = {\n  response: {\n    success: true,\n    message: '',\n    data: {/* the provider's body, verbatim */},\n  },\n  payload: {\n    endPoint: 'https://api.vrio.app/orders?with=items',\n    with: 'items',\n  },\n};\n```\n\n`payload` never contains your API key — it is safe to surface in your own\ndiagnostics.\n\nWhen the provider returns an error the envelope carries it:\n\n```ts\nconst shape = {\n  success: false,\n  message: 'Order not found',\n  validation_code: 'not_found',\n  data: {/* the provider's error body */},\n};\n```\n\nIf the request never reached the provider, the object accessors return\n`{ curlError: '...' }` instead, and `get()` returns the bare message.\n\nThe accessor names also exist on `API` itself, where they always throw\n`VrioError('No API method has been invoked yet')`. Read the result from the\n`Call` the resource method returned.\n\n## Available methods\n\nConsult the [VRIO API reference](https://docs.vrio.com/reference/vrio-api-overview)\nfor the fields each endpoint accepts. `params` is sent as query parameters on\n`GET` calls and as the JSON body on the rest. Every `params` argument is\noptional and typed `Record<string, unknown>`.\n\n`API.supportedMethods()` returns the names of all 18 methods.\n\n### Campaigns\n\n| Method                                          | Request                             |\n| ----------------------------------------------- | ----------------------------------- |\n| `getCampaignItems(campaignId: string, params?)` | `GET /campaigns/{campaignId}/items` |\n\n### Customers\n\n| Method                                     | Request                       |\n| ------------------------------------------ | ----------------------------- |\n| `getCustomer(customerId: string, params?)` | `GET /customers/{customerId}` |\n\n### Offers\n\n| Method                 | Request       |\n| ---------------------- | ------------- |\n| `searchOffer(params?)` | `GET /offers` |\n\n### Routes\n\n| Method                               | Request                 |\n| ------------------------------------ | ----------------------- |\n| `getRoute(routeId: string, params?)` | `GET /routes/{routeId}` |\n\n### Discounts\n\n| Method                       | Request                                                                         |\n| ---------------------------- | ------------------------------------------------------------------------------- |\n| `validateDiscount(params?)`  | `POST /discounts/validate`                                                      |\n| `calculateDiscount(params?)` | `POST /discounts/calculate` — the params are sent as the body's `offers` member |\n\n### Orders\n\n| Method                                     | Request                                                      |\n| ------------------------------------------ | ------------------------------------------------------------ |\n| `searchOrder(params?)`                     | `GET /orders`                                                |\n| `getOrder(orderId: string, params?)`       | `GET /orders/{orderId}`                                      |\n| `addOrder(params?)`                        | `POST /orders`                                               |\n| `editOrder(params?)`                       | `PATCH /orders/{order_id}` — requires `order_id` in `params` |\n| `processOrder(orderId: string, params?)`   | `POST /orders/{orderId}/process`                             |\n| `completeOrder(orderId: string, params?)`  | `POST /orders/{orderId}/complete`                            |\n| `authorizeOrder(orderId: string, params?)` | `POST /orders/{orderId}/authorize`                           |\n| `captureOrder(orderId: string, params?)`   | `POST /orders/{orderId}/capture`                             |\n| `addOrderNote(orderId: string, params?)`   | `POST /orders/{orderId}/notes`                               |\n\n### Carts\n\n| Method                       | Request                                                                     |\n| ---------------------------- | --------------------------------------------------------------------------- |\n| `createCart(params?)`        | `POST /carts`                                                               |\n| `createPaypalToken(params?)` | `POST /carts/{cart_token}/payment_tokens` — requires `cart_token`           |\n| `getPaypalData(params?)`     | `GET /carts/{cart_token}/payment_tokens/{payment_token_id}` — requires both |\n\nExamples:\n\n```ts\nawait api.getCampaignItems('camp_1', { with: 'offers' }).getInObject();\n\nawait api\n  .addOrder({\n    connection_id: 'con_1',\n    campaign_id: 'camp_1',\n    email: 'buyer@example.test',\n  })\n  .getInObject();\n\nawait api.captureOrder('ord_1', { amount: 1000 }).getInObject();\n```\n\n## Errors\n\nEverything the package raises is a `VrioError`, one class, which extends\n`Error`:\n\n```ts\nimport { VrioError } from '@astermd-hq/vrio-client';\n\ntry {\n  const result = await api.getOrder(orderId).getInObject();\n} catch (error) {\n  if (error instanceof VrioError) {\n    // empty API key, non-bare host, missing required argument,\n    // unknown method, or an undecodable response\n  }\n}\n```\n\nThe name is kept from the PHP client so an error branch carried over from that\npackage keeps working.\n\nProvider-side errors and transport failures are **not** thrown — they come back\nin the envelope, as shown above, and a transport failure arrives as\n`{ curlError: '...' }`.\n\n## Debug logging\n\nLogging is off unless you turn it on. When on, entries are redacted by default\nand written as copy-pasteable cURL commands with the response beneath.\n\n```ts\nconst api = new API(apiKey, {\n  debug: true,\n  debugFile: '/var/log/vrio/client.log',\n  debugRetentionDays: 7,\n  debugTimezone: 'UTC',\n});\n```\n\nWhich produces `/var/log/vrio/client-2026-08-16.log` containing:\n\n```\n[2026-08-16 09:14:02.481000 UTC]\ncurl --location --request POST 'https://api.vrio.app/orders' \\\n  --header 'Content-Type: application/json' \\\n  --header 'hostname: api.vrio.app' \\\n  --header 'X-Api-Key: [REDACTED]' \\\n  --data '{\"email\":\"buyer@example.test\",\"card\":{\"number\":\"[REDACTED]\",\"cvv\":\"[REDACTED]\"}}'\n\n# Response: HTTP 201\n{\"id\":\"ord_1\",\"access_token\":\"[REDACTED]\"}\n```\n\nTimestamps carry millisecond resolution written into the microsecond field, so\nthe last three digits are always zero. The format is otherwise identical to the\nPHP client's.\n\n### What is redacted\n\nHeaders and bodies. In headers: `X-Api-Key`, `Authorization` (the scheme is\nkept, so `Bearer [REDACTED]`), `Proxy-Authorization`, `Cookie`. In bodies, by\nfield name: API keys and secrets, passwords, every `*_token` including\n`access_token` and `refresh_token`, card numbers, CVV/CVC, expiry fields, bank\naccount and routing numbers, IBAN, and government identifiers such as SSN, tax\nID and date of birth. Card numbers are additionally caught by shape — any\n13–19 digit string that passes a Luhn check is masked wherever it appears.\n\nA body that is not decodable JSON cannot be field-masked, so it is replaced\nwhole rather than logged on the chance it is harmless.\n\n**Redaction never changes what is sent or what you receive.** The logger reads\nfrom immutable request and response objects and produces a string; the wire\nrequest and the value returned to your code are untouched. The test suite\nasserts this directly.\n\n### The URL is logged verbatim\n\nBy design — you need the real URL for a log entry to be reproducible. That\nmeans anything the API takes in a path segment or query string is written to\nthe log even with redaction on. In this client that is:\n\n| Call                                                                                                                     | What lands in the log                                  |\n| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |\n| `getPaypalData()`                                                                                                        | the cart token and the payment token, both in the path |\n| `createPaypalToken()`                                                                                                    | the cart token, in the path                            |\n| `getCustomer()`                                                                                                          | the customer ID, in the path                           |\n| `getOrder()`, `processOrder()`, `completeOrder()`, `authorizeOrder()`, `captureOrder()`, `addOrderNote()`, `editOrder()` | the order ID, in the path                              |\n| `getCampaignItems()`, `getRoute()`                                                                                       | the campaign or route ID, in the path                  |\n| any `GET` with `params`                                                                                                  | every query parameter you passed, encoded but unmasked |\n\nDo not pass sensitive values as query parameters to `GET` calls if your log\nretention cannot accommodate them.\n\n### Turning redaction off\n\n```ts\nconst api = new API(apiKey, {\n  debug: true,\n  debugRedact: false, // logs the real API key and full bodies\n  debugFile: '/tmp/vrio-debug.log',\n});\n```\n\nThis writes live credentials and complete payloads to disk. It exists for local\ndebugging. **Never enable it in production.**\n\n### File rotation and retention\n\nThe built-in sink writes one file per calendar day, deriving the name from your\nbase path: `/var/log/vrio/client.log` becomes `client-2026-08-16.log`,\n`client-2026-08-17.log`, and so on.\n\nPruning removes files older than `debugRetentionDays` (default 7; `0` keeps\neverything). It only ever matches this package's own dated filename pattern for\nyour base path — other files in the directory are never touched — and it reads\nthe age from the filename rather than the modification time, so an appended-to\nor restored file keeps its true age. It runs once per process, not once per\nrequest.\n\n### Sending logs somewhere else\n\nSupply a function and the file sink is replaced entirely. The package then\nwrites no files, and retention becomes your responsibility:\n\n```ts\nconst api = new API(apiKey, {\n  debug: true,\n  debugSink: (entry: string): void => {\n    myLogger.debug(entry);\n  },\n});\n```\n\nThe sink signature is `(entry: string) => void | Promise<void>`. It receives\nthe finished entry, already redacted unless you opted out, and a returned\npromise is awaited before the call resolves. This is the extension point for\nany external destination — a logging library, a queue, a log shipper, an object\nstore.\n\nA failure inside your sink is caught and discarded: logging must never break an\nAPI call.\n\n### Logs stay sensitive after redaction\n\nA redacted entry still records which account touched which order, cart,\ncustomer and route, and when. Store logs on encrypted volumes, restrict read\naccess, ship them only to systems cleared for that data, and apply a retention\nperiod at least as strict as the rest of your order data.\n\n## Proxy support\n\n```ts\nawait api.withProxy('proxy.example.test:8080', 'user:password').searchOrder().getInObject();\n```\n\nThe setting applies to the next call only.\n\nThe bundled transport uses `fetch` directly and switches to an HTTP `CONNECT`\ntunnel only when a proxy is set, so the default path is unaffected. If your\nplatform gives you a fixed egress address — a NAT gateway with a static IP, for\ninstance — that is usually the better way to satisfy an IP allowlist.\n\n## Custom transport\n\nPass anything implementing `HttpClientInterface` as the third constructor\nargument to route requests through your own stack, or to test without a\nnetwork:\n\n```ts\nimport { API, type HttpClientInterface, Request, Response } from '@astermd-hq/vrio-client';\n\nclass MyTransport implements HttpClientInterface {\n  async send(request: Request): Promise<Response> {\n    // ... hand the request to your own stack, or a fixture ...\n    return new Response(200, body, { http_code: 200 });\n  }\n}\n\nconst api = new API(apiKey, {}, new MyTransport());\n```\n\nAn implementation must never throw on transport failure; report it through the\nresponse so the logger can record the attempt first.\n\nTLS peer and host verification are always on in the bundled transport, and\nthere is no option to disable them.\n\n## Further documentation\n\n- [Integration guide](docs/INTEGRATION_GUIDE.md) — setup, environments,\n  production logging, error handling, troubleshooting.\n- [Architecture](docs/ARCHITECTURE.md) — how a call flows through the package,\n  where to extend it, and how it differs from the PHP client.\n- [Contributing](CONTRIBUTING.md) — the gate, the style rules and the\n  invariants a change must not break.\n- [Security policy](SECURITY.md) — private disclosure and credential handling.\n- [Changelog](CHANGELOG.md)\n\n## Development\n\n```bash\nnpm ci\nnpm run gate     # lint -> typecheck -> test -> build -> dist interop -> publint\n```\n\nSee [CLAUDE.md](CLAUDE.md) for the conventions the gate enforces. No test in\nthis suite makes an external network call.\n\n## Support\n\nEmail **admin@astermd.com**, or open an issue at\n<https://github.com/astermd/vrio-client/issues>. The API reference for the\nendpoints themselves lives in your AsterMD dashboard and at\n<https://docs.vrio.com/reference/vrio-api-overview>.\n\nReport security issues privately — see [SECURITY.md](SECURITY.md). Do not open\na public issue for a vulnerability.\n\n## Compliance\n\nUsing this package does not by itself make your application PCI DSS, HIPAA or\nGDPR compliant. It is one component in your system. Scoping, encryption at\nrest, access control, audit logging, breach procedures and your agreements with\nVRIO and your payment processors remain your responsibility. For licensing or\nBAA enquiries, email **admin@astermd.com**.\n\n## License\n\nMIT — see [LICENSE](LICENSE), including the trademark and affiliation notice.\n","readmeFilename":"README.md","_rev":"1-0dd3e30cb1dd6582a1aa38f5b1ef4b7e"}