{"_id":"@devsym/subscription-portal-s2s-client","_rev":"2-f89e089ddde694caa7d65a34abc52e98","name":"@devsym/subscription-portal-s2s-client","dist-tags":{"latest":"1.1.1"},"versions":{"1.1.0":{"name":"@devsym/subscription-portal-s2s-client","version":"1.1.0","keywords":["subscription-portal","s2s","entitlements","licensing","saas","azure","entra"],"author":{"name":"Devsym GmbH"},"license":"Apache-2.0","_id":"@devsym/subscription-portal-s2s-client@1.1.0","maintainers":[{"name":"thomaspe","email":"thomas@pentenrieder.dev"}],"dist":{"shasum":"d0d0cd353556fd38841d551446d2afd65008ad6e","tarball":"https://registry.npmjs.org/@devsym/subscription-portal-s2s-client/-/subscription-portal-s2s-client-1.1.0.tgz","fileCount":7,"integrity":"sha512-6LfqouP8atPTqGa3TVUZG8WJatt5rf3T9VaqDbqcg8lm7He7YsvdH5JjutRx8j7F+3mkd6WofFvUdWIp/bGTeQ==","signatures":[{"sig":"MEYCIQCfCb3DjQBhZbx2g13FOjyBo8+ye5b+qS7e9YaNqU+tegIhAIemvq7b79Sbf3D6jxPc0iHOqXio+qSFlY4vaK7DhpPU","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":72970},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"78f5278efd2b0306fddfbc1bbce00815722b6a3a","scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --out-dir dist","verify":"node scripts/verify-entrypoints.mjs","prepack":"npm run build && npm run verify"},"_npmUser":{"name":"thomaspe","email":"thomas@pentenrieder.dev"},"_npmVersion":"11.6.4","description":"Typed service-to-service client for the Subscription Management Portal /api/s2s/v1 API.","directories":{},"_nodeVersion":"22.17.0","dependencies":{"@azure/identity":"^4.5.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/subscription-portal-s2s-client_1.1.0_1788906302322_0.7801024579459459","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@devsym/subscription-portal-s2s-client","version":"1.1.1","description":"Typed service-to-service client for the Subscription Management Portal /api/s2s/v1 API.","license":"Apache-2.0","author":{"name":"Devsym GmbH"},"keywords":["subscription-portal","s2s","entitlements","licensing","saas","azure","entra"],"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"}}},"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --out-dir dist","verify":"node scripts/verify-entrypoints.mjs","prepack":"npm run build && npm run verify"},"dependencies":{"@azure/identity":"^4.5.0"},"engines":{"node":">=20"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"gitHead":"bb6ecded5ee696f51cc0ca473de068d39067dbed","_id":"@devsym/subscription-portal-s2s-client@1.1.1","_nodeVersion":"22.23.2","_npmVersion":"11.19.1","dist":{"integrity":"sha512-Nvcal/y3//o7qBXQWqie81OVJa3tiHr+xMqaJ5Oj02ZUf7maO/BNs8VoCJ86iPF6f5RkIsegGIzAM//RSJDtjw==","shasum":"4e111de8f1858d6c5de8c60ddc48a9ed219d5704","tarball":"https://registry.npmjs.org/@devsym/subscription-portal-s2s-client/-/subscription-portal-s2s-client-1.1.1.tgz","fileCount":7,"unpackedSize":72577,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCUWRHa7wuvUgpc0gpZyRfJyc64U+s3/ouHR6kbJJwaIwIgYowuCerxfxFCSuh7/xRo8PapSO8iMUOMedcMP3czJig="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8a9090f7-59ec-4995-a42e-74908a2abd8c"}},"directories":{},"maintainers":[{"name":"thomaspe","email":"thomas@pentenrieder.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/subscription-portal-s2s-client_1.1.1_1788907116909_0.01486309766137861"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-08T22:25:02.138Z","modified":"2026-09-08T22:38:37.218Z","1.1.0":"2026-09-08T22:25:02.470Z","1.1.1":"2026-09-08T22:38:37.072Z"},"author":{"name":"Devsym GmbH"},"license":"Apache-2.0","keywords":["subscription-portal","s2s","entitlements","licensing","saas","azure","entra"],"description":"Typed service-to-service client for the Subscription Management Portal /api/s2s/v1 API.","maintainers":[{"name":"thomaspe","email":"thomas@pentenrieder.dev"}],"readme":"# @devsym/subscription-portal-s2s-client\n\nTyped service-to-service client for the Subscription Management Portal's\n`/api/s2s/v1` API.\n\nThe portal owns subscriptions, licence keys, trials and entitlements. A SaaS\napplication uses this client to ask a portal instance what a Microsoft tenant is\nentitled to, to redeem and release licence keys on that tenant's behalf, to start\na trial, and to request a provider sync.\n\nThe normative interface is the OpenAPI contract shipped with the portal\n(`contracts/s2s/v1/openapi.yaml`); this package is a convenience client over it\nfor TypeScript callers.\n\n## Install\n\n```bash\nnpm install @devsym/subscription-portal-s2s-client\n```\n\nRequires Node 20 or later. Both ESM (`import`) and CommonJS (`require`) entry\npoints are published, with matching type declarations for each.\n\n## Usage\n\n```ts\nimport { PortalS2sClient } from \"@devsym/subscription-portal-s2s-client\";\n\nconst portal = new PortalS2sClient({\n  baseUrl: \"https://portal.example.com\",\n  audience: \"api://<s2s-resource-app-id>\",\n});\n\nconst entitlements = await portal.getEntitlements(appId, microsoftTenantId);\nif (entitlements.hasActiveEntitlement) {\n  seatLimit = entitlements.aggregateEffectiveQuantity;\n}\n```\n\n`baseUrl` is the portal instance you were onboarded to, and `audience` is the\napplication ID URI of that instance's S2S resource registration. Both are\nper-instance: a client built for one portal must never be pointed at another.\nOnly HTTPS origins are accepted (plus HTTP loopback for local development), and\nthe URL may not carry userinfo, a query, or a fragment.\n\n### Authentication\n\nBy default the client acquires a token with `DefaultAzureCredential` from\n`@azure/identity`, requesting `${audience}/.default`. In Azure that means a\nmanaged identity and no secret in your application configuration. Follow the\nportal's S2S Entra setup guide to register your caller and have it enabled as a\ntrusted application with the roles your calls need.\n\nTo supply tokens another way, pass any object with a matching `getToken`:\n\n```ts\nconst portal = new PortalS2sClient({\n  baseUrl,\n  audience,\n  credential: { getToken: async (scope) => ({ token: await mint(scope) }) },\n});\n```\n\n### Operations\n\n| Method | Route | Role |\n| --- | --- | --- |\n| `getCapabilities` | `GET .../capabilities` | *(none)* |\n| `getEntitlements` | `GET .../tenants/{tid}/entitlements` | `Entitlement.Read` |\n| `getSubscriptionStatus` | `GET .../tenants/{tid}/subscription-status` | `Subscription.Read` |\n| `getLinkage` | `GET .../tenants/{tid}/linkage` | `Linkage.Read` |\n| `redeemLicenseKey` | `POST .../claims/redeem-license-key` | `Claim.Redeem` |\n| `releaseClaim` | `POST .../claims/release` | `Claim.Release` |\n| `startTrial` | `POST .../tenants/{tid}/trial` | `Trial.Start` |\n| `requestTenantSync` | `POST .../tenants/{tid}/sync` | `Sync.Request` |\n| `requestSubscriptionSync` | `POST .../subscriptions/{subscriptionId}/sync` | `Sync.Request` |\n\nEvery mutating call sends a generated `Idempotency-Key`, and every call sends an\n`x-correlation-id` that is echoed back on `PortalS2sError.correlationId` — quote\nit when asking a portal operator to look something up.\n\n## Errors\n\nAll failures reject with `PortalS2sError`, carrying `code`, `status`,\n`correlationId` and optional `details`. Codes originating in the client:\n\n| Code | Meaning |\n| --- | --- |\n| `token_unavailable` | The credential returned no token, or threw. No request was sent. |\n| `transport_error` | The portal was unreachable after the built-in retries. |\n| `endpoint_unavailable` | This portal instance does not serve the route — see below. |\n| `unexpected_response` | An off-contract status or body. Provider detail is deliberately not surfaced. |\n\nAny other code comes from the portal's own error envelope and is passed through\nverbatim.\n\nRequests are retried with jittered backoff on 408, 429 and 5xx, honouring\n`Retry-After`; a retried mutating call reuses its original idempotency key.\n\n## Version compatibility\n\nPortal instances are self-hosted, so the client can be newer than the instance it\ntalks to.\n\n- **The package major is locked to the contract major.** This client speaks\n  `/api/s2s/v1`, exported as `S2S_API_VERSION`. A new contract major ships as a\n  new package major, never as a minor.\n- **Minors are additive.** A minor may add methods for routes an older portal\n  does not serve yet. Calling one against such an instance rejects with\n  `endpoint_unavailable` rather than a generic failure, so it is distinguishable\n  from a route that exists and legitimately returned 404.\n- **Check at startup rather than on first use.** `getCapabilities` reports what\n  the instance serves and what the caller may do, and needs no role:\n\n  ```ts\n  const { operations, grantedRoles, portalVersion } = await portal.getCapabilities(appId);\n  for (const required of [\"getEntitlements\", \"startTrial\"]) {\n    if (!operations.includes(required)) {\n      throw new Error(`Portal ${portalVersion ?? \"(unknown build)\"} does not serve ${required}`);\n    }\n  }\n  if (!grantedRoles.includes(\"Trial.Start\")) {\n    throw new Error(\"Trial.Start is not granted to this caller\");\n  }\n  ```\n\n  `grantedRoles` is the intersection of the roles in your access token and the\n  roles on your trusted-application record, so a role granted on only one of the\n  two sides shows up as missing here instead of failing later with\n  `insufficient_role`. Treat `portalVersion` as diagnostics only — branch on\n  `operations`, never on the version string.\n- **Treat entitlement reads as cacheable.** The portal does not push entitlement\n  changes. Cache the last successful projection per tenant and serve it if the\n  portal is unreachable — fail open to the last known state, never to \"no\n  entitlement\".\n\n## Licence\n\nApache-2.0. This licence covers the client package only, not the Subscription\nManagement Portal itself.\n","readmeFilename":"README.md"}