{"_id":"@advcomm/uids-io-auth","_rev":"4-f69904318e6115fbd5963fa24b952655","name":"@advcomm/uids-io-auth","dist-tags":{"latest":"1.1.0"},"versions":{"0.1.0":{"name":"@advcomm/uids-io-auth","version":"0.1.0","keywords":["auth","oauth","oidc","jwt","express","postgresql"],"license":"MIT","_id":"@advcomm/uids-io-auth@0.1.0","maintainers":[{"name":"syedhashmi","email":"hashmi@gmail.com"},{"name":"dev1-hc","email":"dev1@hostingcontroller.com"},{"name":"dev2t","email":"dev2@advcomm.ca"}],"dist":{"shasum":"a659563bf9bde4a75b2027c83b376a7abfc7887a","tarball":"https://registry.npmjs.org/@advcomm/uids-io-auth/-/uids-io-auth-0.1.0.tgz","fileCount":11,"integrity":"sha512-n+lR+HVVKcs0VV2YpCxdO5t0H/GIjC5L3xaDANG9j4XvOqMp8KLCdtOITnIO42k6B94xL5UoIxQEq68MA+rF8Q==","signatures":[{"sig":"MEUCIAjT7szIknaM1yB0O72b8cXOK1IqEVedLGNmNBSrShaFAiEAw0Vt9ZShfxaDzh1YYP/4rKNWLYmljSkdHJ6VpTLLhSA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":508730},"main":"./dist/index.js","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./dist/index.js","default":"./dist/index.js"},"./express":{"import":"./dist/express/index.js","default":"./dist/express/index.js"}},"gitHead":"a5a555e72b7819372767efab1d9c84266ab45d0b","scripts":{"dev":"tsup --watch","lint":"biome lint --write src/","test":"vitest run","build":"tsup && node scripts/copy-migrations.mjs","check":"biome check --write src/","format":"biome format --write src/","test:unit":"vitest run tests/unit","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build","test:integration":"vitest run tests/integration"},"_npmUser":{"name":"dev2t","email":"dev2@advcomm.ca"},"_npmVersion":"10.9.3","description":"Production-ready auth package with OAuth/OIDC, sessions, devices, and Express integration","directories":{},"_nodeVersion":"22.19.0","dependencies":{"zod":"^3.24.2","jose":"^6.0.11","@node-rs/argon2":"^2.0.2"},"_hasShrinkwrap":false,"devDependencies":{"pg":"^8.13.3","tsx":"^4.19.3","tsup":"^8.4.0","pg-mem":"^3.0.5","vitest":"^3.0.8","express":"^4.21.2","@types/pg":"^8.11.11","supertest":"^7.0.0","typescript":"^5.8.2","@types/node":"^22.13.10","@biomejs/biome":"2.4.16","@types/express":"^5.0.0","@types/supertest":"^6.0.2","@testcontainers/postgresql":"^12.0.1"},"peerDependencies":{"pg":"^8.11.0","express":"^4.18.0 || ^5.0.0"},"peerDependenciesMeta":{"express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/uids-io-auth_0.1.0_1780392729093_0.05447913428853557","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@advcomm/uids-io-auth","version":"1.0.0","keywords":["auth","oauth","oidc","jwt","express","postgresql"],"license":"MIT","_id":"@advcomm/uids-io-auth@1.0.0","maintainers":[{"name":"syedhashmi","email":"hashmi@gmail.com"},{"name":"dev1-hc","email":"dev1@hostingcontroller.com"},{"name":"dev2t","email":"dev2@advcomm.ca"}],"homepage":"https://github.com/uids-io/auth#readme","bugs":{"url":"https://github.com/uids-io/auth/issues"},"dist":{"shasum":"9ea29230873c4e8a9d91a2eca8238267323eeefb","tarball":"https://registry.npmjs.org/@advcomm/uids-io-auth/-/uids-io-auth-1.0.0.tgz","fileCount":15,"integrity":"sha512-AOwMHKKhQrLV19AESL85/oeObiardu4sVHJ+MAKZsnUBUATWX+n0aRUzRrIRkHnJ2lo07z07Hsyd+IdDWIAyOw==","signatures":[{"sig":"MEYCIQDJqLaRhWES7sIFuLWz0kzTYfFB5fIxUBb3NNIfJJSoogIhAIgkEPTduy1wNR05hotSnvOvJTZf/Jd20yP1AlgbjXBC","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":595538},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./express":{"types":"./dist/express/index.d.ts","import":"./dist/express/index.js","default":"./dist/express/index.js"}},"gitHead":"db93ae8252ecffc638ff68ef3f36732206939bcc","scripts":{"dev":"tsup --watch","lint":"biome lint --write src/","test":"vitest run","build":"tsup && node scripts/copy-migrations.mjs","check":"biome check --write src/","format":"biome format --write src/","release":"semantic-release","test:unit":"vitest run tests/unit","typecheck":"tsc --noEmit","npm:latest":"npm view @advcomm/uids-io-auth version","test:watch":"vitest","npm:versions":"npm view @advcomm/uids-io-auth versions --json","release:major":"npm version major","release:minor":"npm version minor","release:patch":"npm version patch","prepublishOnly":"npm run build && node scripts/assert-npm-version.cjs","release:dry-run":"semantic-release --dry-run","test:integration":"vitest run tests/integration"},"_npmUser":{"name":"dev2t","email":"dev2@advcomm.ca"},"repository":{"url":"git+https://github.com/uids-io/auth.git","type":"git"},"_npmVersion":"10.9.3","description":"Production-ready auth package with OAuth/OIDC, sessions, devices, and Express integration","directories":{},"_nodeVersion":"22.19.0","dependencies":{"zod":"^3.24.2","jose":"^6.0.11","pino":"^9.6.0","@node-rs/argon2":"^2.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"pg":"^8.13.3","tsx":"^4.19.3","tsup":"^8.4.0","pg-mem":"^3.0.5","vitest":"^3.0.8","express":"^4.21.2","@types/pg":"^8.11.11","supertest":"^7.0.0","typescript":"^5.8.2","@types/node":"^22.13.10","pino-pretty":"^13.0.0","@biomejs/biome":"2.4.16","@types/express":"^5.0.0","@types/supertest":"^6.0.2","semantic-release":"^25.0.3","@semantic-release/git":"^10.0.1","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.6","@testcontainers/postgresql":"^12.0.1","@semantic-release/changelog":"^6.0.3"},"peerDependencies":{"pg":"^8.11.0","express":"^4.18.0 || ^5.0.0"},"peerDependenciesMeta":{"express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/uids-io-auth_1.0.0_1780902561811_0.4667886588103707","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@advcomm/uids-io-auth","version":"1.0.1","keywords":["auth","oauth","oidc","jwt","express","postgresql"],"license":"MIT","_id":"@advcomm/uids-io-auth@1.0.1","maintainers":[{"name":"syedhashmi","email":"hashmi@gmail.com"},{"name":"dev1-hc","email":"dev1@hostingcontroller.com"},{"name":"dev2t","email":"dev2@advcomm.ca"}],"homepage":"https://github.com/uids-io/auth#readme","bugs":{"url":"https://github.com/uids-io/auth/issues"},"dist":{"shasum":"e24edaa02bc3c382f060901d51f4acd739b915aa","tarball":"https://registry.npmjs.org/@advcomm/uids-io-auth/-/uids-io-auth-1.0.1.tgz","fileCount":15,"integrity":"sha512-XgbJhd04DpdJ+wZoThexyh+NlmT/KJQTQUkGQq11Wh0Y+i64LRyW8be3+RVhU/9JpT6Yc1SkMpoiEZAYCW49Bw==","signatures":[{"sig":"MEQCIH+kel+j+C1WfK9POmQ38uc6hTwsaBbPIo0OFVKpj7FHAiB3TKYL1Ct5iG2UIXwYpkrKqVGLl3mnE34m+XKkdcxCrw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":601586},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./express":{"types":"./dist/express/index.d.ts","import":"./dist/express/index.js","default":"./dist/express/index.js"}},"gitHead":"d5d1987a460a22d0e7d7af852c4a8622cc95fffd","scripts":{"dev":"tsup --watch","lint":"biome lint --write src/","test":"vitest run","build":"tsup && node scripts/copy-migrations.mjs","check":"biome check --write src/","format":"biome format --write src/","release":"semantic-release","test:unit":"vitest run tests/unit","typecheck":"tsc --noEmit","npm:latest":"npm view @advcomm/uids-io-auth version","test:watch":"vitest","npm:versions":"npm view @advcomm/uids-io-auth versions --json","release:major":"npm version major","release:minor":"npm version minor","release:patch":"npm version patch","prepublishOnly":"npm run build && node scripts/assert-npm-version.cjs","release:dry-run":"semantic-release --dry-run","test:integration":"vitest run tests/integration"},"_npmUser":{"name":"dev2t","email":"dev2@advcomm.ca"},"repository":{"url":"git+https://github.com/uids-io/auth.git","type":"git"},"_npmVersion":"11.16.0","description":"Production-ready auth package with OAuth/OIDC, sessions, devices, and Express integration","directories":{},"_nodeVersion":"24.16.0","dependencies":{"zod":"^3.24.2","jose":"^6.0.11","pino":"^9.6.0","@node-rs/argon2":"^2.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"pg":"^8.13.3","tsx":"^4.19.3","tsup":"^8.4.0","pg-mem":"^3.0.5","vitest":"^3.0.8","express":"^4.21.2","@types/pg":"^8.11.11","supertest":"^7.0.0","typescript":"^5.8.2","@types/node":"^22.13.10","pino-pretty":"^13.0.0","@biomejs/biome":"2.4.16","@types/express":"^5.0.0","@types/supertest":"^6.0.2","semantic-release":"^25.0.3","@semantic-release/git":"^10.0.1","@semantic-release/npm":"^13.1.5","@semantic-release/github":"^12.0.6","@testcontainers/postgresql":"^12.0.1","@semantic-release/changelog":"^6.0.3"},"peerDependencies":{"pg":"^8.11.0","express":"^4.18.0 || ^5.0.0"},"peerDependenciesMeta":{"express":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/uids-io-auth_1.0.1_1780923424932_0.33352412441418466","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@advcomm/uids-io-auth","version":"1.1.0","description":"Production-ready auth package with OAuth/OIDC, sessions, devices, and Express integration","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./express":{"types":"./dist/express/index.d.ts","import":"./dist/express/index.js","default":"./dist/express/index.js"}},"scripts":{"build":"tsup && node scripts/copy-migrations.mjs","dev":"tsup --watch","test":"vitest run","test:unit":"vitest run tests/unit","test:integration":"vitest run tests/integration","test:watch":"vitest","typecheck":"tsc --noEmit","npm:versions":"npm view @advcomm/uids-io-auth versions --json","npm:latest":"npm view @advcomm/uids-io-auth version","release":"semantic-release","release:dry-run":"semantic-release --dry-run","release:patch":"npm version patch","release:minor":"npm version minor","release:major":"npm version major","prepublishOnly":"npm run build && node scripts/assert-npm-version.cjs","format":"biome format --write src/","lint":"biome lint --write src/","check":"biome check --write src/"},"engines":{"node":">=20"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0","pg":"^8.11.0"},"peerDependenciesMeta":{"express":{"optional":true}},"dependencies":{"@node-rs/argon2":"^2.0.2","jose":"^6.0.11","pino":"^9.6.0","zod":"^3.24.2"},"repository":{"type":"git","url":"git+https://github.com/uids-io/auth.git"},"bugs":{"url":"https://github.com/uids-io/auth/issues"},"homepage":"https://github.com/uids-io/auth#readme","publishConfig":{"access":"public"},"devDependencies":{"@semantic-release/changelog":"^6.0.3","@semantic-release/git":"^10.0.1","@semantic-release/github":"^12.0.6","@semantic-release/npm":"^13.1.5","@biomejs/biome":"2.4.16","@testcontainers/postgresql":"^12.0.1","@types/express":"^5.0.0","@types/node":"^22.13.10","@types/pg":"^8.11.11","@types/supertest":"^6.0.2","express":"^4.21.2","pg":"^8.13.3","pg-mem":"^3.0.5","pino-pretty":"^13.0.0","semantic-release":"^25.0.3","supertest":"^7.0.0","tsup":"^8.4.0","tsx":"^4.19.3","typescript":"^5.8.2","vitest":"^3.0.8"},"license":"MIT","keywords":["auth","oauth","oidc","jwt","express","postgresql"],"gitHead":"70e4946fb353900f90c9fc5c814792f1b8285df0","_id":"@advcomm/uids-io-auth@1.1.0","_nodeVersion":"24.16.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-CZiW2SJJLvWO/Z55g1wtkO1z9jfTaoOTRkep3za+Ht9svO6xAFHHjN6zzdCKgFYYXR7M8NTa/QOcR9bR04zOmQ==","shasum":"0c26516013c43083ec2f00a5f0928db7c45ff930","tarball":"https://registry.npmjs.org/@advcomm/uids-io-auth/-/uids-io-auth-1.1.0.tgz","fileCount":15,"unpackedSize":607746,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDLA4/vptAiAJVXQRRE7T7PMxo0AaCxii/CIYjtwh8NRQIgW/CoXBDDoTKfm4DxI5r9KI8dQyVRXZISNEILEakuk84="}]},"_npmUser":{"name":"dev2t","email":"dev2@advcomm.ca"},"directories":{},"maintainers":[{"name":"syedhashmi","email":"hashmi@gmail.com"},{"name":"dev1-hc","email":"dev1@hostingcontroller.com"},{"name":"dev2t","email":"dev2@advcomm.ca"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/uids-io-auth_1.1.0_1781184475339_0.540876369527119"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-02T09:32:08.894Z","modified":"2026-06-11T13:27:55.620Z","0.1.0":"2026-06-02T09:32:09.276Z","1.0.0":"2026-06-08T07:09:21.984Z","1.0.1":"2026-06-08T12:57:05.090Z","1.1.0":"2026-06-11T13:27:55.522Z"},"bugs":{"url":"https://github.com/uids-io/auth/issues"},"license":"MIT","homepage":"https://github.com/uids-io/auth#readme","keywords":["auth","oauth","oidc","jwt","express","postgresql"],"repository":{"type":"git","url":"git+https://github.com/uids-io/auth.git"},"description":"Production-ready auth package with OAuth/OIDC, sessions, devices, and Express integration","maintainers":[{"name":"syedhashmi","email":"hashmi@gmail.com"},{"name":"dev1-hc","email":"dev1@hostingcontroller.com"},{"name":"dev2t","email":"dev2@advcomm.ca"}],"readme":"# @advcomm/uids-io-auth\n\nProduction-ready authentication for Node.js backends: OAuth 2.0/OIDC, sessions, refresh tokens, SDK-registered device tracking, and an optional Express adapter.\n\nThe package is **not Express-only** — services (`AuthService`, `TokenService`, etc.) are framework-agnostic. Use `createAuthRouter` when you want a ready-made HTTP surface on Express.\n\n## Installation\n\n```bash\nnpm install @advcomm/uids-io-auth pg express\n```\n\n| Dependency | Role |\n|------------|------|\n| `pg` | Required — PostgreSQL access |\n| `express` | Optional peer — only needed for `createAuthRouter` / `requireAuth` |\n\nSubpath export (if you split Express-only imports):\n\n```typescript\nimport { createAuthRouter } from '@advcomm/uids-io-auth/express';\n```\n\n## Environment variables\n\nUse these in your auth server (see [`examples/express-auth-server/.env.example`](examples/express-auth-server/.env.example)):\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `DATABASE_URL` | Yes | PostgreSQL connection string |\n| `ISSUER` | Yes | Public auth issuer URL (e.g. `https://auth.example.com`) |\n| `API_AUDIENCE` | Yes | Resource server audience for access tokens |\n| `CSRF_SECRET` | Yes (prod) | Secret for signing CSRF/session cookies — **must** be set explicitly in production (no fallback) |\n| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | If using Google | OAuth client credentials |\n| `MICROSOFT_CLIENT_ID` / `MICROSOFT_CLIENT_SECRET` | If using Microsoft | Entra app credentials |\n| `MICROSOFT_TENANT` | No | Default `common` |\n| `LOG_LEVEL` | No | Pino level: `debug`, `info`, `warn`, `error` (default: `debug` in dev, `info` in production) |\n\nRegister OAuth clients for each portal with `OAuthClientService.upsertPublicClient` (see [Portal OAuth clients](#portal-oauth-clients)). The example auth server uses a local seed helper, not a package export.\n\n## Database migrations\n\nMigrations are **not** run automatically. Call explicitly on startup:\n\n```typescript\nimport { Pool } from 'pg';\nimport { runAuthMigrations } from '@advcomm/uids-io-auth';\n\nconst pool = new Pool({ connectionString: process.env.DATABASE_URL });\nawait runAuthMigrations(pool);\n```\n\n## Auth server (auth.example.com)\n\n```typescript\nimport express from 'express';\nimport { Pool } from 'pg';\nimport {\n  createAuthKit,\n  createAuthRouter,\n  OAuthClientService,\n  runAuthMigrations,\n} from '@advcomm/uids-io-auth';\n\nconst pool = new Pool({ connectionString: process.env.DATABASE_URL });\nawait runAuthMigrations(pool);\n\n// Register one public OAuth client per portal (PKCE). Repeat per app.\nconst oauthClients = new OAuthClientService(pool);\nawait oauthClients.upsertPublicClient({\n  id: 'merchant_portal_web',\n  name: 'Merchant Portal Web',\n  redirectUris: ['https://merchant.example.com/auth/callback'],\n});\n\nconst authKit = await createAuthKit({\n  issuer: process.env.ISSUER!,\n  apiAudience: process.env.API_AUDIENCE!,\n  pg: pool,\n  cookie: {\n    name: 'uids_auth_session',\n    domain: '.example.com',\n    secure: true,\n    sameSite: 'lax',\n  },\n  csrf: { secret: process.env.CSRF_SECRET! },\n  providers: {\n    google: {\n      clientId: process.env.GOOGLE_CLIENT_ID!,\n      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n      callbackUrl: `${process.env.ISSUER}/oauth/google/callback`,\n    },\n    microsoft: {\n      clientId: process.env.MICROSOFT_CLIENT_ID!,\n      clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,\n      tenant: process.env.MICROSOFT_TENANT ?? 'common',\n      callbackUrl: `${process.env.ISSUER}/oauth/microsoft/callback`,\n    },\n  },\n  email: {\n    sendMagicLink: async (email, url) => {\n      // integrate with your email provider\n    },\n  },\n});\n\nconst app = express();\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use('/', createAuthRouter(authKit));\napp.listen(3000);\n```\n\n`createAuthRouter` mounts:\n\n- **OIDC** — `/.well-known/openid-configuration`, `/.well-known/jwks.json`, `/login`\n- **OAuth** — `/authorize`, `/token`, provider callbacks, `/logout`\n- **Email** — magic link and password login routes\n- **Sessions** — session cookie introspection and revoke\n- **Devices** — register, list, revoke (CSRF-protected where required)\n- **Middleware** — CORS, CSRF on state-changing routes, Zod validation, centralized error handling\n\nSee [`examples/express-auth-server`](examples/express-auth-server).\n\n**API docs (Bruno):** Import OpenCollection [`bruno/uids-auth-api`](bruno/uids-auth-api) into your Bruno workspace alongside backend service collections — see [`bruno/README.md`](bruno/README.md).\n\n## API server (api.example.com)\n\n```typescript\nimport express from 'express';\nimport { requireAuth } from '@advcomm/uids-io-auth';\n\nconst app = express();\napp.use(express.json());\n\napp.use(requireAuth({\n  issuer: process.env.ISSUER!,\n  audience: process.env.API_AUDIENCE!,\n  jwksUrl: `${process.env.ISSUER}/.well-known/jwks.json`,\n}));\n\napp.get('/me', (req, res) => {\n  res.json({ auth: req.auth });\n});\n```\n\nConfigure CORS on the API to allow your portal origins. This package does not set API CORS headers.\n\nSee [`examples/express-api-server`](examples/express-api-server).\n\n## Errors and logging\n\nThe Express adapter uses two response shapes so OAuth clients and REST portals both get usable errors.\n\n### Validation errors (Zod, HTTP 422)\n\nRoutes validated with the built-in middleware return **all** field issues:\n\n```json\n{\n  \"success\": false,\n  \"message\": \"Validation failed\",\n  \"error\": {\n    \"code\": \"VALIDATION_ERROR\",\n    \"details\": [\n      { \"field\": \"client_id\", \"message\": \"Required\" },\n      { \"field\": \"platform\", \"message\": \"Invalid enum value...\" }\n    ]\n  }\n}\n```\n\nUse `ValidationError`, `isValidationError`, and `ValidationDetail` from the package if you handle errors in custom middleware.\n\n### OAuth / auth errors (HTTP 4xx)\n\nBusiness and OAuth-style failures use the familiar shape:\n\n```json\n{\n  \"error\": \"invalid_request\",\n  \"error_description\": \"Invalid refresh token\"\n}\n```\n\nOther exported errors: `UnauthorizedError`, `ForbiddenError`, `ConflictError`, `RateLimitError`, `InvalidRequestError`, and base `AuthError`.\n\n### Server errors (HTTP 500)\n\nUnexpected errors return a generic body (no stack or internal details). Full error context is logged server-side only.\n\n### Structured logs (Pino)\n\nThe router logs via **Pino** to stdout:\n\n| Situation | Level | What is logged |\n|-----------|-------|----------------|\n| Request validation failed | `warn` | scope, field names, issue count (not request body values) |\n| Expected auth errors | `info` | error code, status, method, path |\n| Unexpected errors | `error` | error name/message, method, path |\n\nSet `LOG_LEVEL=debug` locally. In production, logs are JSON (no pretty-print).\n\n**Tracing:** pass `X-Request-Id` from your gateway or API; it is included in log context when present.\n\n## Google Cloud OAuth setup\n\n1. Create an OAuth 2.0 Client ID (Web application) in Google Cloud Console.\n2. Authorized redirect URI: `https://auth.example.com/oauth/google/callback`\n3. Set `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in your auth server environment.\n\n## Microsoft Entra setup\n\n1. Register an application in Microsoft Entra ID.\n2. Add redirect URI: `https://auth.example.com/oauth/microsoft/callback`\n3. Create a client secret.\n4. Set `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`, and configure `tenant` (`common`, `organizations`, `consumers`, or a tenant ID).\n\n## Portal OAuth clients\n\nEach portal is a **public** OAuth client (PKCE, no client secret in the browser):\n\n```typescript\nimport { OAuthClientService } from '@advcomm/uids-io-auth';\n\nconst oauthClients = new OAuthClientService(pool);\nawait oauthClients.upsertPublicClient({\n  id: 'your_portal_web',\n  name: 'Your Portal Web',\n  redirectUris: ['https://your-portal.example.com/auth/callback'],\n  // origins optional — derived from redirect URIs when omitted\n});\n```\n\nFor local dev with multiple UIDs portals, see [`examples/express-auth-server/seedPortalClients.ts`](examples/express-auth-server/seedPortalClients.ts) (`merchant_portal_web`, `agency_portal_web`, etc.). That helper is **not** exported from the package.\n\n## Login flow (PKCE)\n\n1. Portal generates PKCE verifier/challenge and optional SDK `device_id`.\n2. Portal redirects user to `GET /authorize?response_type=code&client_id=...&redirect_uri=...&scope=openid profile email&state=...&code_challenge=...&code_challenge_method=S256`\n3. User authenticates on auth domain (Google, Microsoft, or email).\n4. Auth domain redirects to portal `redirect_uri?code=...&state=...`\n5. Portal calls `POST /token` with `grant_type=authorization_code`, `code`, `code_verifier`, `client_id`, `redirect_uri`.\n6. Portal receives `access_token`, `refresh_token`, and optional `id_token`.\n7. Portal calls API with `Authorization: Bearer {access_token}`.\n\n## Device identity\n\nCompanion client SDKs (React, Flutter, native) generate a stable UUID `device_id`, register it via `POST /devices/register`, and send `X-Uids-Device-Id` on auth flows. The auth server binds devices to users and includes `device_id` in access token claims.\n\nSupported platforms: `web`, `ios`, `android`, `desktop`, `unknown` (validated on register).\n\nSee [docs/sdk-contract.md](docs/sdk-contract.md) for the full client/server contract.\n\n### Recommended companion SDKs (future packages)\n\n| Platform | Package | Storage |\n|----------|---------|---------|\n| React / Next.js | `@uids-io/auth-react` | localStorage / IndexedDB |\n| Flutter web + mobile | `@uids-io/auth-flutter` | shared_preferences / Keychain |\n| iOS / Android native | `@uids-io/auth-native` | Keychain / EncryptedSharedPreferences |\n| Desktop | `@uids-io/auth-react` or native wrapper | OS keychain |\n\n## Exports\n\n**Kit & HTTP**\n\n- `createAuthKit`, `createAuthRouter`, `requireAuth`\n- `runAuthMigrations`\n- `verifyAccessToken`, `generatePkcePair`, `verifyCodeChallenge`\n\n**Services** (use directly without Express)\n\n- `AuthService`, `UserService`, `TokenService`, `SessionService`, `DeviceService`, `OAuthClientService`\n\n**Errors**\n\n- `AuthError`, `InvalidRequestError`, `ValidationError`, `UnauthorizedError`, `ForbiddenError`, `ConflictError`, `RateLimitError`\n- `isAuthError`, `isValidationError`, `ValidationDetail`\n\n**Types & helpers**\n\n- `AuthUser`, `AuthContext`, `Device`, `DevicePlatform`, `TokenResponse`, provider mappers, etc.\n\n## Testing\n\n```bash\nnpm test              # all tests\nnpm run test:unit     # crypto, redirect_uri, provider mapping\nnpm run test:integration  # DB + Express flows (uses pg-mem by default)\nnpm run typecheck\nnpm run build\n```\n\nIntegration tests use **pg-mem** by default (no Docker required). Optional backends:\n\n- `TEST_DATABASE_URL=postgres://...` — run against an existing PostgreSQL instance\n- `USE_TESTCONTAINERS=1` — use Docker testcontainers when available\n\n## Releases\n\nReleases on **`main`** use **semantic-release** — see [RELEASING.md](RELEASING.md). Use [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, etc.) so version bumps and npm publish happen automatically.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}