{"_id":"@dszp/888voip-lib","_rev":"2-b4fce9e55b59d2161056b3799d59264f","name":"@dszp/888voip-lib","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@dszp/888voip-lib","version":"0.1.0","keywords":["888voip","distributor","voip","inventory","cloudflare-workers"],"author":{"url":"https://david.szpunar.com","name":"David Szpunar"},"license":"MIT","_id":"@dszp/888voip-lib@0.1.0","maintainers":[{"name":"dszp","email":"davidszp@gmail.com"}],"homepage":"https://github.com/dszp/888voip-lib#readme","bugs":{"url":"https://github.com/dszp/888voip-lib/issues"},"dist":{"shasum":"b2baa822ddb26ff0c537754d855bb24481efacd0","tarball":"https://registry.npmjs.org/@dszp/888voip-lib/-/888voip-lib-0.1.0.tgz","fileCount":45,"integrity":"sha512-pLnPTp7tQiD41c7FsSVrJBZvcSgD95xh0T0ndEOCPYm2Kb/dgP70q5oHjDKS3+AEX3SA9oNn2LkLfL0qYQXxGQ==","signatures":[{"sig":"MEUCIQC4S/2OLoWCzKKtlMS2qGquWINXouidh1o4BJcqjXxskQIgX7MeQdXmsLYJENVR8/yE3pCL2TjxqqyITvxqdhKvY64=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":156717},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","//files":"The maps and `src` ship together on purpose. tsc writes a sourceMappingURL pointer into every dist file, and the .d.ts.map references ../src/*.ts — so publishing the pointers WITHOUT them gave consumers a 404 and no Go-to-Definition: the cost of the flags with none of the benefit. With these, a Worker stack trace names a real .ts line and Go-to-Definition opens actual source. Same shape in all four libs — change all of them or none.","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js","require":"./dist/index.js","//require":"ESM-only package. This `require` condition lets Node >=22.12 load it via require(esm); older Node still gets an accurate ERR_REQUIRE_ESM here instead of the confusing ERR_PACKAGE_PATH_NOT_EXPORTED the bare export map produced. Same shape as the three sibling libs — change all four or none."},"./package.json":"./package.json"},"gitHead":"0794faad213e1a2729eba6516461951c18c2ced3","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.test.json --noEmit","test:watch":"vitest"},"_npmUser":{"name":"dszp","email":"davidszp@gmail.com"},"repository":{"url":"git+https://github.com/dszp/888voip-lib.git","type":"git"},"_npmVersion":"11.16.0","description":"Portable, Node-free 888VoIP Channel Advantage toolkit: a read-only API client, an injected cache interface, and the normalisations their API's quirks require. Runs unchanged in a Cloudflare Worker, Node, or the browser.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.11.0","devDependencies":{"tsx":"^4.23.12","vitest":"^3","typescript":"^5"},"_npmOperationalInternal":{"tmp":"tmp/888voip-lib_0.1.0_1787856975003_0.8487528450434549","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@dszp/888voip-lib","version":"0.1.1","description":"Portable, Node-free 888VoIP Channel Advantage toolkit: a read-only API client, an injected cache interface, and the normalisations their API's quirks require. Runs unchanged in a Cloudflare Worker, Node, or the browser.","type":"module","license":"MIT","repository":{"type":"git","url":"git+https://github.com/dszp/888voip-lib.git"},"bugs":{"url":"https://github.com/dszp/888voip-lib/issues"},"homepage":"https://github.com/dszp/888voip-lib#readme","publishConfig":{"access":"public"},"author":{"name":"David Szpunar","url":"https://david.szpunar.com"},"engines":{"node":">=20"},"keywords":["888voip","distributor","voip","inventory","cloudflare-workers"],"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","//require":"ESM-only package. This `require` condition lets Node >=22.12 load it via require(esm); older Node still gets an accurate ERR_REQUIRE_ESM here instead of the confusing ERR_PACKAGE_PATH_NOT_EXPORTED the bare export map produced. Same shape as the three sibling libs — change all four or none.","require":"./dist/index.js","default":"./dist/index.js"},"./package.json":"./package.json"},"types":"./dist/index.d.ts","module":"./dist/index.js","main":"./dist/index.js","sideEffects":false,"//files":"The maps and `src` ship together on purpose. tsc writes a sourceMappingURL pointer into every dist file, and the .d.ts.map references ../src/*.ts — so publishing the pointers WITHOUT them gave consumers a 404 and no Go-to-Definition: the cost of the flags with none of the benefit. With these, a Worker stack trace names a real .ts line and Go-to-Definition opens actual source. Same shape in all four libs — change all of them or none.","scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.test.json --noEmit","//verify":"Build, then actually IMPORT the built entry point with Node. `pnpm build` and `vitest run` BOTH pass on a dist that cannot be loaded: tsc emits relative specifiers verbatim and vitest resolves extensionless ones, so 0.1.0 shipped to npm throwing ERR_MODULE_NOT_FOUND on first import. moduleResolution NodeNext now makes the compiler refuse the bad form; this is the backstop that checks the artefact a consumer actually gets, rather than the sources it was built from.","verify":"pnpm build && node --input-type=module -e \"import('./dist/index.js').then(m => { if (!Object.keys(m).length) { throw new Error('dist/index.js exported nothing'); } console.log('verify: dist imports cleanly,', Object.keys(m).length, 'exports'); })\"","test":"vitest run","test:watch":"vitest"},"devDependencies":{"tsx":"^4.23.12","typescript":"^5","vitest":"^3"},"packageManager":"pnpm@11.11.0","gitHead":"c7104a02a985148d0adc26b6bbe0c14162a9ba0d","_id":"@dszp/888voip-lib@0.1.1","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-+4Oces2Hh13kuDLu9BzL9x3PHDh7raYUP9zP4nASpxUwh35fLz2ndFoSkCjuQ2QQwr8mYAInX3I3z5vUAYoSTw==","shasum":"651c95a4836e0ee68d8dc1c7d8f63371e94afbdc","tarball":"https://registry.npmjs.org/@dszp/888voip-lib/-/888voip-lib-0.1.1.tgz","fileCount":45,"unpackedSize":157612,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@dszp%2f888voip-lib@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDGxIqN/NMoFtU5d9CfZkKdUJg569nfX/vZSgLUcJIZ4AiEAk7TOvvO2xGXBNHSr7U9VLdV4szY0nvlI1WrOlGwKXhQ="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:75932d3b-1833-4e96-afa5-7a62cc2072ea"}},"directories":{},"maintainers":[{"name":"dszp","email":"davidszp@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/888voip-lib_0.1.1_1787956267140_0.524998787655909"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-27T18:56:14.804Z","modified":"2026-08-28T22:31:07.608Z","0.1.0":"2026-08-27T18:56:15.145Z","0.1.1":"2026-08-28T22:31:07.274Z"},"bugs":{"url":"https://github.com/dszp/888voip-lib/issues"},"author":{"name":"David Szpunar","url":"https://david.szpunar.com"},"license":"MIT","homepage":"https://github.com/dszp/888voip-lib#readme","keywords":["888voip","distributor","voip","inventory","cloudflare-workers"],"repository":{"type":"git","url":"git+https://github.com/dszp/888voip-lib.git"},"description":"Portable, Node-free 888VoIP Channel Advantage toolkit: a read-only API client, an injected cache interface, and the normalisations their API's quirks require. Runs unchanged in a Cloudflare Worker, Node, or the browser.","maintainers":[{"name":"dszp","email":"davidszp@gmail.com"}],"readme":"# @dszp/888voip-lib\n\nPortable, Node-free toolkit for the [888VoIP](https://888voip.com) Channel Advantage API:\na read-only client, an injected cache interface, and the normalisations their API's quirks\nrequire. Zero dependencies. Runs unchanged in a Cloudflare Worker, Node, or the browser.\n\n## Install\n\n```sh\npnpm add @dszp/888voip-lib\n```\n\n## Use\n\n```ts\nimport { VoipClient, memoryCache, poRefOf } from '@dszp/888voip-lib';\n\nconst client = new VoipClient({\n  baseUrl: 'https://api.888voip.com',\n  token: process.env.VOIP888_TOKEN!,\n  cache: memoryCache(),\n});\n\nconst page = await client.getOrders({ newestFirst: true });\nfor (const order of page.orders) {\n  console.log(order.orderNumber, order.orderStatus, poRefOf(order));\n}\n```\n\nOne cache, two servers, and the keys must say which is which — the client scopes every key to\nthe host of its `baseUrl`, so running staging beside production over a single KV namespace is\nsafe. Pass `cacheNamespace` only to separate two accounts on one host, and never pass the token:\nkeys get logged, listed and enumerated.\n\n⚠️ **If you use 888VoIP's provisioning service, a cached order carries live SIP credentials.**\nAn order placed with a `provisioning` block reads back with that block intact — your provisioning\nserver's `srvUser`/`srvPass`, and a `login`/`pin` per extension — so anything that caches the order\nis now storing them. Back the cache with something private and short-lived, and do not log what it\nround-trips. Orders placed without a provisioning block carry no credentials, and the field is\nabsent rather than empty.\n\n## Read only\n\n`VoipClient` exposes only GETs, and this package ships no write client. Hold one and you know\nit cannot place an order. Credentials are minted and revoked by `createToken` / `revokeToken`\nin their own module, never as methods on the client.\n\n## Caching is not optional in practice\n\nThe upstream per-minute limits are tight — a single order is **5/min**, the order and product\nlists **10/min**. Pass a `cache`:\n\n| Runtime | What to pass |\n|---|---|\n| Cloudflare Worker | a thin adapter over a KV namespace |\n| Node script, tests | `memoryCache()` |\n| No caching | omit it, and mind the limits |\n\n`VoipCache` is two methods over strings — `get(key)` and `put(key, value, ttlSeconds)` — so\nnothing here is bound to any one runtime. The TTLs and key format are the client's business, not\nyours; pass `onCacheRead` if you want to see whether the cache answered a given call, which under\nlimits this tight is the only view you have of your own headroom.\n\n```ts\nconst kvCache = {\n  get: (k: string) => env.VOIP_CACHE.get(k),\n  put: (k: string, v: string, ttl: number) => env.VOIP_CACHE.put(k, v, { expirationTtl: ttl }),\n};\n```\n\n## Two behaviours worth knowing\n\n**An account with no orders answers HTTP 400**, not an empty list. `getOrders` maps that one\nmessage to an empty page; any other 400 still throws.\n\n**`getOrder` returns `null` for a missing order** rather than throwing, because \"this account\ncannot see that order\" is an ordinary answer — an order placed on a sister company's account, or\none predating the API, reaches you as `null`. Upstream signals that with `400 {\"message\":\"Order\nnot found.\"}` rather than a 404, so both are read as not-found. A 500, or a 400 saying anything\nelse, still throws: not-found and upstream-broken are different facts.\n\n## API\n\n| Export | What it does |\n|---|---|\n| `VoipClient` | `getProducts`, `getProduct`, `getCategories`, `getOrders`, `getOrder`, `getPrivateWarehouses` |\n| `createToken` / `revokeToken` / `revokeAllTokens` | Credential management. Tokens never expire; revocation is the only rotation. |\n| `memoryCache` | An in-process `VoipCache`, for Node scripts and tests. Not for a Worker — module state dies with the isolate. |\n| `poRefOf` | The purchase-order reference you gave the vendor, from either of the two field names it arrives under |\n| `normalizeProduct`, `normalizeCategories`, `decodeHtmlEntities`, `htmlToMarkdown` | The upstream quirks, handled |\n| `VoipApiError` | Carries `status` and `body` |\n| `VoipShapeError` | A 200 that is not the envelope the endpoint documents — upstream answered, just not with something readable |\n\n## License\n\nMIT\n","readmeFilename":"README.md"}