{"_id":"@elegantys/polisafe","name":"@elegantys/polisafe","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@elegantys/polisafe","version":"0.1.0","description":"SDK NestJS para integrar @Permission/@Scope y autenticacion JWT/OpenID contra un servidor Polisafe (Polizei).","keywords":["nestjs","polizei","polisafe","oauth2","openid","jwt","permissions","rbac","security"],"license":"MIT","author":{"name":"William Amed","email":"@elegantys"},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.js","default":"./dist/index.js"},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"engines":{"node":">=18"},"scripts":{"build":"tsc -p tsconfig.build.json","prepack":"npm run build","pack:dry":"npm pack --dry-run"},"peerDependencies":{"@nestjs/axios":"^3.0.2","@nestjs/common":"^10.0.0","@nestjs/core":"^10.0.0","@nestjs/jwt":"^10.2.0","@nestjs/passport":"^11.0.5","@nestjs/schedule":"^4.1.2","jwks-rsa":"^3.2.0","passport":"^0.7.0","passport-jwt":"^4.0.1","reflect-metadata":"^0.1.13","rxjs":"^7.8.1"},"devDependencies":{"@nestjs/axios":"^3.0.2","@nestjs/common":"^10.0.0","@nestjs/core":"^10.0.0","@nestjs/jwt":"^10.2.0","@nestjs/passport":"^11.0.5","@nestjs/schedule":"^4.1.2","@types/express":"^4.17.17","@types/node":"^20.3.1","@types/passport-jwt":"^4.0.1","jwks-rsa":"^3.2.0","passport":"^0.7.0","passport-jwt":"^4.0.1","reflect-metadata":"^0.1.13","rxjs":"^7.8.1","typescript":"^5.1.3"},"_id":"@elegantys/polisafe@0.1.0","_nodeVersion":"22.23.2","_npmVersion":"12.0.2","dist":{"integrity":"sha512-uf6RqNWkY28KOoH2nlglMStaUSFS5ok3Fp05tHnsCHXk9OQsUSibbufvW8nFJ7GhNE82niCV9GKSYM0zZwJUvQ==","shasum":"1dff12e27a58fe88805d48c5fb021a08ccf246e9","tarball":"https://registry.npmjs.org/@elegantys/polisafe/-/polisafe-0.1.0.tgz","fileCount":46,"unpackedSize":89556,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQClvz+AwaVkP76oWF1Sd4rkFaY4AoUAW+HgfO8i4mZZywIhAIGnkPSIoOJbVcY5Kw/qIhOF8DrnUFmSDUXTk7uvqjpS"}]},"_npmUser":{"name":"williamamed","email":"watamayo90@gmail.com"},"directories":{},"maintainers":[{"name":"williamamed","email":"watamayo90@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/polisafe_0.1.0_1788492199209_0.2295885259938315"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-04T03:23:18.999Z","0.1.0":"2026-09-04T03:23:19.369Z","modified":"2026-09-04T03:23:19.581Z"},"maintainers":[{"name":"williamamed","email":"watamayo90@gmail.com"}],"description":"SDK NestJS para integrar @Permission/@Scope y autenticacion JWT/OpenID contra un servidor Polisafe (Polizei).","keywords":["nestjs","polizei","polisafe","oauth2","openid","jwt","permissions","rbac","security"],"author":{"name":"William Amed","email":"@elegantys"},"license":"MIT","readme":"# @elegantys/polisafe\n\n**NestJS client SDK for Polisafe IAM** — protect your REST APIs with the authentication and authorization power of [Polisafe](https://elegantys.net), the multi-tenant Identity & Access Management platform (formerly *Polizei*).\n\nDrop-in decorators (`@Permission`, `@Scope`, `@UserToken`, …), guards and Passport strategies that validate the RS256 JWTs issued by a **Polisafe IAM** server and resolve authorization — locally from token claims or remotely against the IAM server — so your services never re-implement auth.\n\n---\n\n## Table of contents\n\n- [What it does](#what-it-does)\n- [How it works](#how-it-works)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Quick start](#quick-start)\n- [Configuration options](#configuration-options)\n- [Decorators](#decorators)\n- [Authorization model](#authorization-model)\n- [Token types & auth flows](#token-types--auth-flows)\n- [Guards & strategies](#guards--strategies)\n- [TypeScript](#typescript)\n- [Server API contract](#server-api-contract)\n- [Troubleshooting](#troubleshooting)\n- [Development](#development)\n- [License](#license)\n\n---\n\n## What it does\n\n- **Authenticates** every request by validating the `Authorization: Bearer` JWT (or a session cookie) against the **Polisafe IAM** server's per-tenant JWKS (`RS256`).\n- **Resolves the tenant** of the request from the token and the optional `X-Tenant` header.\n- **Authorizes** the route: the permission is checked either **remotely** (the IAM server decides, `POST …/polizei/io/authorization`) or **locally** against the `permissions` claims embedded in the access token.\n- **Speaks machine-to-machine**: it obtains and automatically renews a `client_credentials` access token so it can call the IAM server's authorization API on behalf of your service.\n- Keeps everything typed: `TokenPayload`, config options and decorators ship with full TypeScript declarations.\n\n> Polisafe IAM is a technology of [Elegantys](https://elegantys.net). This package is the *client-side* utility: it consumes the APIs published by a Polisafe IAM server. You still need a running Polisafe IAM instance and an OAuth client registered on it.\n\n---\n\n## How it works\n\n```\n                ┌──────────────────────┐        ┌────────────────────────────┐\n  Browser/App ──▶  Your NestJS service │  HTTP  │  Polisafe IAM server       │\n  (Bearer JWT)   │  @elegantys/polisafe├───────▶│  • OAuth2 / OIDC token     │\n                 │  • jwt-header        │        │  • per-tenant JWKS (/certs)│\n                 │  • tenant guard      │        │  • io/authorization check  │\n                 │  • privilege guard   │        └────────────────────────────┘\n                 └──────────────────────┘\n```\n\n1. A user signs in against **Polisafe IAM** and receives an RS256 JWT (OIDC) carrying claims such as `sub`, `tid`/`tenantId`, `tenants`, optionally `permissions`, `scope`, `client_id`.\n2. Your route declares what it needs with a decorator (`@Permission('orders/list')`).\n3. The SDK's Passport strategy decodes the token, extracts the tenant, downloads the matching public key from `…/polisafe/openid/{tenant}/certs` and verifies the signature.\n4. Guards resolve the active tenant and decide allow/deny.\n5. When remote checks are enabled, the SDK authenticates itself with `client_credentials` and asks the IAM server whether `sub` may perform `{url, method}` in its tenant.\n\n---\n\n## Requirements\n\n- **Node.js ≥ 18**\n- **NestJS 10** (`@nestjs/common`, `@nestjs/core`, `@nestjs/platform-express`)\n- A reachable **Polisafe IAM** server and an **OAuth client** registered on it with:\n  - the `client_credentials` grant, and\n  - the scopes `security:io:authorization` (remote checks) and optionally `security:io:tenants`.\n\n`peerDependencies` are declared in `package.json`; npm ≥ 7 installs them automatically in the consuming project.\n\n---\n\n## Installation\n\n```bash\n# from a registry\nnpm install @elegantys/polisafe\n\n# or from a local checkout while developing\nnpm install ../sdk-polisafe\n```\n\n---\n\n## Quick start\n\nRegister the module **once** in your root module:\n\n```ts\n// app.module.ts\nimport { Module } from '@nestjs/common';\nimport { PolisafeSdkModule } from '@elegantys/polisafe';\n\n@Module({\n  imports: [\n    PolisafeSdkModule.register({\n      serviceUrl: process.env.POLISAFE_URL,          // e.g. https://iam.example.com/api\n      issuer: process.env.POLISAFE_ISSUER,           // e.g. https://iam.example.com\n      client_id: process.env.POLISAFE_CLIENT_ID,     // your client_credentials client\n      client_secret: process.env.POLISAFE_CLIENT_SECRET,\n      scope: 'security:io:authorization',            // enables remote permission checks\n      mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\nProtect a REST controller:\n\n```ts\n// orders.controller.ts\nimport { Body, Controller, Get, Post } from '@nestjs/common';\nimport { Permission, TokenPayload, UserToken } from '@elegantys/polisafe';\n\n@Controller('orders')\n@Permission() // class level: every route in the controller is protected\nexport class OrdersController {\n  @Get()\n  list(@UserToken() user: TokenPayload) {\n    return { user: user.sub, tenant: user.tenant, items: [] };\n  }\n\n  @Post()\n  @Permission('orders/create') // more specific, overrides nothing: metadata is additive\n  create(@Body() body: unknown, @UserToken() user: TokenPayload) {\n    return { createdBy: user.id };\n  }\n}\n```\n\nThe SDK registers `JwtModule`, `HttpModule` and `ScheduleModule` internally — you don't have to import anything else.\n\n---\n\n## Configuration options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `serviceUrl` | `string` | — (**required**) | Base URL of the Polisafe IAM server (public URL + path prefix), e.g. `https://iam.example.com/api` |\n| `issuer` | `string` | — (**required**) | Expected `iss` claim of incoming tokens, usually the IAM public URL |\n| `client_id` | `string` | — | OAuth `client_credentials` client id (needed for remote checks) |\n| `client_secret` | `string` | — | OAuth `client_credentials` client secret (needed for remote checks) |\n| `scope` | `string` | — | Scopes requested for the machine token, e.g. `security:io:authorization` |\n| `mode` | `'production' \\| 'development'` | `'development'` | `development` enables verbose error messages and debug logging |\n| `permissionCheck` | `boolean` | `true` | Set to `false` to **disable permission checks entirely** — development only! |\n| `checkProviderPermissions` | `boolean` | `true` | `true`: ask the IAM server (`…/polizei/io/authorization`); `false`: validate against `user.permissions` embedded in the token |\n| `audience` | `string` | — | Expected audience (currently informational; not enforced by default) |\n| `logging` | `boolean` | `false` | Reserved for future use |\n| `apiKey` | `string` | — | **Deprecated** — legacy workspace key, do not use |\n\nOptions without defaults are merged over the SDK defaults at registration time, so you only need to pass what differs.\n\n---\n\n## Decorators\n\n### `@Permission(name?)`\n\nAuthenticates (`jwt-header` strategy) → resolves the tenant (`TenantGuard`) → authorizes the route (`PrivilegesGuard`).\n\n- Works at **method** or **class** level.\n- `name` is the permission identifier registered in Polisafe IAM. When omitted, the guard falls back to the request's route path (`/orders`, `POST /orders`).\n\n```ts\n@Get('by-id/:id')\n@Permission('orders:read')\nread(@UserToken() user: TokenPayload) {\n  return user;\n}\n```\n\n### `@Scope(scopes, tokenType?)`\n\nAuthenticates + tenant + validates **OAuth scopes** in the token's `scope` claim, optionally restricting the token type.\n\n```ts\n// only machine (client_credentials) tokens holding this scope may call this route\n@Get('internal/tenants')\n@Scope('security:io:tenants', 'client')\nlistTenants(@UserToken() root: TokenPayload) {\n  return { service: root.client_id };\n}\n```\n\n`tokenType` accepts `'user'`, `'client'` or `'all'` (default). The SDK tells them apart with `sub === client_id`.\n\n### `@UserToken()` / `@TokenInfo()`\n\nInjects the verified token payload with convenience aliases:\n\n```ts\n@Get('me')\n@Permission('profile:read')\nme(@UserToken() user: TokenPayload) {\n  // user.sub            — subject (string)\n  // user.id             — alias of Number(user.sub)\n  // user.username       — alias of preferred_username\n  // user.tid / .tenant  — active tenant\n  // user.tenants        — all tenants the principal belongs to\n  // user.is_client      — sub === client_id\n}\n```\n\n`@TokenInfo()` is the same contract (originally used on OIDC `userinfo`-style endpoints).\n\n### `@MapTenant(key)`\n\nMaps a request parameter (`query` or `body`) to one of the principal's tenants, returning `406` if the value is not inside `user.tenants`. Useful on multi-tenant admin routes to prevent cross-tenant access.\n\n```ts\n@Delete()\n@Permission('orders:delete')\nremove(@Body() body: Record<string, any>, @MapTenant('idScope') tenant: string) {\n  return { tenant }; // guaranteed to belong to the caller\n}\n```\n\n---\n\n## Authorization model\n\n**Permission names.** In Polisafe IAM, permissions are stored per tenant and assigned to roles → users. The string you pass to `@Permission()` is matched by name (`route.name`), and optionally by HTTP method (`route.type`).\n\n**Two check modes** (see `PrivilegesGuard`):\n\n| `checkProviderPermissions` | Where the decision happens | Requirement |\n|---|---|---|\n| `true` (default) | IAM server — `POST …/polizei/io/authorization` with the machine token | `client_id`/`client_secret`/`scope` configured; IAM reachable |\n| `false` | Your service — token's `permissions` claims | IAM client with `access_token_include_permissions` / `openid_include_permissions` enabled |\n\n**Tenant resolution.** If the token lists several tenants (`user.tenants`), the active one comes from the `X-Tenant` request header and must be present in that list; otherwise the first tenant is used. A token cannot act outside its tenant boundaries.\n\n**Development mode.** With `mode: 'development'` and `permissionCheck: false`, every request is allowed and guards return detailed error messages. Never ship this configuration to production — the SDK logs a warning when it detects it.\n\n---\n\n## Token types & auth flows\n\n**User tokens** — obtained from Polisafe IAM's OAuth 2.0 / OpenID Connect endpoints (`authorization_code`, `password`, `implicit`, `refresh_token`). Send them as:\n\n```\nAuthorization: Bearer <access_token>\n```\n\n**Machine tokens** — the SDK obtains its own via the OAuth 2.0 `client_credentials` grant:\n\n- `POST {serviceUrl}/polisafe/oauth/token` with HTTP Basic authentication (`client_id:client_secret`) and the configured `scope`.\n- The token is cached in the shared options object and **renewed automatically**: a scheduled job runs every minute and refreshes it when less than 120 seconds remain.\n- Used as `Authorization: Bearer <machine-token>` for remote permission checks.\n\n**Cookie sessions** — for browser flows, the IAM server stores the token in a cookie named `a-<client_id>`. Use the cookie strategy:\n\n```ts\nimport { JwtAuthCookieGuard } from '@elegantys/polisafe';\n\n@Get('spa')\n@UseGuards(JwtAuthCookieGuard)\nspa(@UserToken() user: TokenPayload) {\n  return user;\n}\n```\n\nRemember to enable `cookie-parser` in your Nest app and to send `client_id` (query or body) so the SDK can locate the cookie.\n\n---\n\n## Guards & strategies\n\nEverything is exported for reuse or composition:\n\n| Export | Kind | Notes |\n|---|---|---|\n| `PolisafeSdkModule` | module | `register(options)` returns a `@Global` dynamic module |\n| `JwtStrategyService` | Passport strategy | name `'jwt-header'`, Bearer token, per-tenant JWKS |\n| `JwtCookieStrategyService` | Passport strategy | name `'jwt-cookie'`, cookie `a-<client_id>` |\n| `PolizeiSdkService` | service | machine `client_credentials` lifecycle |\n| `JwtAuthGuard` / `JwtAuthCookieGuard` | guards | wrap the two strategies |\n| `TenantGuard`, `PrivilegesGuard`, `ScopesGuard` | guards | authorization building blocks |\n| `ConfigPolizei`, `POLIZEI_CONFIG_OPTIONS` | config | typed options + NestJS injection token |\n| `TokenPayload` | type | claims of a verified Polisafe token |\n\nCompose your own guards if the decorators are not enough:\n\n```ts\nimport { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';\nimport { JwtAuthGuard, PrivilegesGuard, TenantGuard } from '@elegantys/polisafe';\n\n@Injectable()\nexport class MyFullAuthGuard extends JwtAuthGuard implements CanActivate {\n  // e.g. run TenantGuard + PrivilegesGuard programmatically in canActivate()\n}\n```\n\nThe strategies verify tokens **only against Polisafe IAM's per-tenant JWKS** — no shared secrets, no hardcoded keys, and key rotation is handled by `jwks-rsa` caching.\n\n---\n\n## TypeScript\n\nThe package ships declarations. `TokenPayload` reflects the standard OIDC claims plus Polisafe's tenant/RBAC claims:\n\n```ts\ninterface TokenPayload {\n  sub: string;                 // subject\n  iss: string; aud: string | string[];\n  exp: number; iat: number; jti?: string;\n  client_id?: string;\n  name?: string; given_name?: string; family_name?: string;\n  nickname?: string; preferred_username?: string;\n  picture?: string; email?: string; email_verified?: boolean;\n  roles?: string[]; permissions?: string[];\n  tid?: string; tenant?: string; tenants?: string[];\n  id: number;                  // Number(sub)\n  is_client?: boolean;         // sub === client_id\n}\n```\n\nNeed the options elsewhere? Inject them with full typing:\n\n```ts\nimport { Inject } from '@nestjs/common';\nimport { ConfigPolizei, POLIZEI_CONFIG_OPTIONS } from '@elegantys/polisafe';\n\nexport class SomeService {\n  constructor(@Inject(POLIZEI_CONFIG_OPTIONS) private readonly options: ConfigPolizei) {}\n}\n```\n\n---\n\n## Server API contract\n\nThe SDK consumes the following Polisafe IAM endpoints. They are part of the public IAM surface; a future version of the SDK may pin an explicit API version.\n\n| Endpoint | Purpose |\n|---|---|\n| `POST {serviceUrl}/polisafe/oauth/token` (`client_credentials`) | obtain / renew the machine token |\n| `GET {serviceUrl}/polisafe/openid/{tenant}/certs` | per-tenant JWKS used to verify RS256 signatures |\n| `POST {serviceUrl}/polizei/io/authorization` | remote permission decision: body `{ url, method, sub }`, `200` = allowed |\n| `GET {serviceUrl}/polizei/io/tenants` | (optional) tenant listing for machine clients |\n\nThe IAM server must issue tokens containing `tid`/`tenantId` and `tenants`; without them the strategies refuse the token (`No tenant identifier found in token`).\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause / fix |\n|---|---|\n| `401` on every protected route | Token missing, expired, wrong `issuer`, or the IAM server is unreachable for JWKS |\n| `No tenant identifier found in token` | Token lacks `tid`/`tenantId` — check the IAM client scopes/config |\n| `401 Tenant boundaries out of scope` | `X-Tenant` not included in `user.tenants` — omit the header or send a valid tenant |\n| `403 Forbidden` (remote mode) | IAM says the user has no `{url, method}` permission — grant it in the IAM admin, or `client_id`/`client_secret`/`scope` are not configured |\n| `403 Forbidden` (local mode) | Token has no matching `permissions` entry — enable `access_token_include_permissions` on the client |\n| Everything is allowed | `permissionCheck: false` (dev) is active — never enable in production |\n| Errors show only in `development` | By design: production hides internals; guard messages depend on `options.mode` |\n\n---\n\n## Development\n\n```bash\nnpm install          # install dev/peer dependencies\nnpm run build        # tsc -> dist/\nnpm pack --dry-run   # inspect the publishable tarball\n```\n\nNotes for maintainers:\n\n- `peerDependencies` target **NestJS 10**.\n- The package is published from `dist/` only (`files: [\"dist\", \"README.md\"]`).\n- If `npm install` fails with `EALLOWSCRIPTS` under npm ≥ 12, unset the injected `npm_config_allow_scripts` environment variable for the command, or use `--ignore-scripts` (none of the runtime dependencies require lifecycle scripts).\n\n---\n\n## License\n\nMIT — Polisafe IAM is a technology of Elegantys. This package is an independent client utility and does not include the IAM server.\n","readmeFilename":"README.md","_rev":"1-ae794c368a6bcb0e3b7a01d922c961fd"}