{"_id":"@advcomm/uids-io-auth-react","name":"@advcomm/uids-io-auth-react","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.1":{"name":"@advcomm/uids-io-auth-react","version":"1.0.1","description":"Browser/React OAuth client for @advcomm/uids-io-auth (PKCE, devices, refresh)","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"}},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","npm:versions":"npm view @advcomm/uids-io-auth-react versions --json","npm:latest":"npm view @advcomm/uids-io-auth-react 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/ tests/ examples/","lint":"biome lint --write src/ tests/ examples/","check":"biome check --write src/ tests/ examples/","check:ci":"biome check src/ tests/","validate":"npm run check:ci && npm run typecheck && npm test && npm run build","prepare":"husky","example:portal":"npm run build && npm run dev --prefix examples/vite-example-portal"},"engines":{"node":">=20"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react-dom":{"optional":true}},"devDependencies":{"@biomejs/biome":"2.4.16","@semantic-release/changelog":"^6.0.3","@semantic-release/git":"^10.0.1","@semantic-release/github":"^12.0.6","@semantic-release/npm":"^13.1.5","@testing-library/react":"^16.3.0","@types/react":"^19.1.2","@types/react-dom":"^19.1.2","@vitest/coverage-v8":"^4.1.8","husky":"^9.1.7","jsdom":"^26.0.0","react":"^19.1.0","react-dom":"^19.1.0","semantic-release":"^25.0.3","tsup":"^8.4.0","typescript":"^5.8.2","vitest":"^4.1.8"},"license":"MIT","keywords":["auth","oauth","pkce","react","oids","uids"],"repository":{"type":"git","url":"git+https://github.com/uids-io/sdk_react.git"},"bugs":{"url":"https://github.com/uids-io/sdk_react.git/issues"},"homepage":"https://github.com/uids-io/sdk_react.git#readme","publishConfig":{"access":"public"},"gitHead":"83bfd894b2554ca7579f53f6ea3bac08d6c197cc","_id":"@advcomm/uids-io-auth-react@1.0.1","_nodeVersion":"24.16.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-v5f+YQ333dV5pYQb7+v08nylg7ka4g3WeC9tEH2/z5pKPUjjQ8ZHBN2loAvK7G6OEQngDCER3blAxTLFVWoemQ==","shasum":"e72a7e490b7597d0c03ccd75c22d1506a463d5ab","tarball":"https://registry.npmjs.org/@advcomm/uids-io-auth-react/-/uids-io-auth-react-1.0.1.tgz","fileCount":7,"unpackedSize":202817,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIG9CW6D5vlEP7QhRl1jed4mybGsN38zbHoKwEFa139b+AiEAuQZPH3XD95Yge5EaSYVriiDErG3z2v6zqexZbB0hfiM="}]},"_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-react_1.0.1_1780989414302_0.7587603828666831"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-09T07:16:54.191Z","1.0.1":"2026-06-09T07:16:54.529Z","modified":"2026-06-09T07:16:54.800Z"},"maintainers":[{"name":"syedhashmi","email":"hashmi@gmail.com"},{"name":"dev1-hc","email":"dev1@hostingcontroller.com"},{"name":"dev2t","email":"dev2@advcomm.ca"}],"description":"Browser/React OAuth client for @advcomm/uids-io-auth (PKCE, devices, refresh)","homepage":"https://github.com/uids-io/sdk_react.git#readme","keywords":["auth","oauth","pkce","react","oids","uids"],"repository":{"type":"git","url":"git+https://github.com/uids-io/sdk_react.git"},"bugs":{"url":"https://github.com/uids-io/sdk_react.git/issues"},"license":"MIT","readme":"# @advcomm/uids-io-auth-react\n\nBrowser/React OAuth client for [`@advcomm/uids-io-auth`](https://github.com/advcomm/uids-io-auth). Implements the [client SDK contract](../auth/docs/sdk-contract.md): PKCE, device binding, refresh rotation, and SPA logout.\n\n**Minimum server version:** `@advcomm/uids-io-auth` `>=0.1.0`\n\n**Example app:** [`examples/vite-example-portal`](./examples/vite-example-portal)\n\n**Code documentation:**\n\n- **[Token storage & multi-tab security](./docs/TOKEN_STORAGE.md)** — cookie vs body, rotation, critical behavior\n- **[Login providers](./docs/LOGIN_PROVIDERS.md)** — Google / Microsoft / email discovery & `signIn({ provider })`\n- **[OAuth redirect plan](./docs/OAUTH_REDIRECT_PLAN.md)** — callback transaction, `useAuthCallback`, Strict Mode fix (implementation reference)\n- [Architecture & module map](./docs/ARCHITECTURE.md)\n- **IDE hovers** — JSDoc on `AuthClient`, `AuthReactConfig`, etc. (after `npm run build`)\n\n---\n\n## What this package does\n\n| Component | Role |\n|-----------|------|\n| **Auth server** (`issuer`) | Runs `@advcomm/uids-io-auth` — login UI, `/authorize`, `/token`, `/refresh`, `/logout`, devices |\n| **This SDK** | Your React app — PKCE redirect, tokens, hooks, API `fetch` helper |\n| **API server** (`apiAudience`) | Your backend — validates Bearer tokens via `requireAuth` |\n\n```mermaid\nflowchart LR\n  App[React app] -->|PKCE OAuth| Auth[Auth server issuer]\n  App -->|Bearer access_token| API[API server]\n  Auth -->|JWT signed with issuer| API\n```\n\nEach app build has **one** `clientId` + **one** `redirectUri`. All apps share the same `issuer` (and usually the same `apiAudience`).\n\n---\n\n## Install\n\n```bash\nnpm install @advcomm/uids-io-auth-react\n```\n\n**Peer dependency:** React 18 or 19.\n\n---\n\n## Integration checklist\n\nUse this when wiring a new React app:\n\n1. **Register OAuth client** on the auth server (`OAuthClientService.upsertPublicClient`) with this app’s `redirect_uri` and allowed origin.\n2. **Env vars** — `VITE_AUTH_ISSUER`, `VITE_AUTH_CLIENT_ID`, `VITE_AUTH_REDIRECT_URI` (names may differ for CRA/Next).\n3. **Callback route** — e.g. `/auth/callback` uses `useAuthCallback()` (strips `?code`, exchanges once).\n4. **Root** — wrap the app in `<AuthProvider config={...}>`.\n5. **Login page** — call `loadProviders()` before rendering provider sign-in buttons.\n6. **API client** — Bearer from `getAccessToken()`; on 401 refresh once, else `signIn()` (`createAuthFetch`).\n7. **Logout** — `signOut()` with cookie or body refresh token.\n8. **CORS** — auth server allows your app’s `Origin`; API server must allow the app origin in production (or use a BFF/proxy).\n\n\n---\n\n## Configuration\n\n### Environment variables (Vite)\n\n```bash\n# Auth server base URL — include mount path if router is not at host root\n# Examples: http://localhost:3000  or  http://localhost:3000/auth\nVITE_AUTH_ISSUER=http://localhost:3000\n\n# Unique per app (must match upsertPublicClient on the auth server)\nVITE_AUTH_CLIENT_ID=my_app_web\n\n# Must match an allowed redirect URI for that client\nVITE_AUTH_REDIRECT_URI=http://localhost:5173/auth/callback\n\n# Optional: your API base URL (app → API, not auth)\nVITE_API_URL=https://api.example.com\n```\n\n### `AuthReactConfig`\n\n| Field | Required | Default | Description |\n|-------|----------|---------|-------------|\n| `issuer` | Yes | — | Auth server base URL (see below) |\n| `clientId` | Yes | — | OAuth public client id for **this** app only |\n| `redirectUri` | Yes | — | Exact callback URL registered on the server |\n| `platform` | No | `\"web\"` | Sent on device register / authorize |\n| `appVersion` | No | — | Sent on device register |\n| `scope` | No | `openid profile email` | Authorize scope |\n| `deviceStorage` | No | `localStorage` | `localStorage` or `indexedDB` for `device_id` |\n| `tokenDelivery` | No | `auto` | `auto` → cookie (browser web), `cookie`, or `body` (JSON RT) |\n| `tokenStorage` | No | `sessionStorage` | Only when `tokenDelivery: \"body\"` — where AT+RT persist |\n| `apiAudience` | No | — | Client-side hint only; API server enforces audience |\n| `refreshSkewSeconds` | No | `60` | Refresh this many seconds before access token expiry |\n\n**`issuer`** is the full public base URL of `createAuthRouter`, **including any mount path** (e.g. `https://auth.example.com` or `http://localhost:3000/auth`). It is **not** your business API URL unless you colocate auth and API on one host.\n\nKeep `config` referentially stable (e.g. `useMemo` or module-level constant) so `AuthProvider` does not recreate the client every render.\n\n```ts\nimport type { AuthReactConfig } from \"@advcomm/uids-io-auth-react\";\n\nexport const authConfig: AuthReactConfig = {\n  issuer: import.meta.env.VITE_AUTH_ISSUER,\n  clientId: import.meta.env.VITE_AUTH_CLIENT_ID,\n  redirectUri: import.meta.env.VITE_AUTH_REDIRECT_URI,\n  platform: \"web\",\n};\n```\n\n---\n\n## OAuth client registration\n\nEach React app build uses **one** `clientId` and **one** `redirectUri`, registered on the auth server via `OAuthClientService.upsertPublicClient`. To add another app, register a new public client and redirect URI; only env/config changes in that React project.\n\nFor seeded local-dev client IDs, ports, and the included Vite example, see [`examples/vite-example-portal/README.md`](./examples/vite-example-portal/README.md).\n\n---\n\n## Step-by-step integration\n\n### 1. Wrap the app\n\n```tsx\nimport { AuthProvider } from \"@advcomm/uids-io-auth-react\";\nimport { authConfig } from \"./auth/config\";\n\nexport function AppRoot({ children }: { children: React.ReactNode }) {\n  return <AuthProvider config={authConfig}>{children}</AuthProvider>;\n}\n```\n\n`AuthProvider` on mount: restores the session via `initialize()` (silent refresh when a cookie/body session exists). Device registration runs on first `signIn()`. Call `loadProviders()` from the login page (or set `loadProvidersOnMount`).\n\n### 2. Routing (React Router example)\n\n| Path | Component | Purpose |\n|------|-----------|---------|\n| `/` | Home | Sign-in button |\n| `/auth/callback` | Callback | Exchange `code` for tokens |\n| `/dashboard` | Protected | `useRequireAuth()` or manual guard |\n\nSee [`examples/vite-example-portal/src/App.tsx`](./examples/vite-example-portal/src/App.tsx).\n\n### 3. Callback page\n\nUse `useAuthCallback` — do not hand-roll `useEffect` + `handleCallback` (unsafe under React Strict Mode).\n\n```tsx\nimport { useNavigate } from \"react-router-dom\";\nimport { useAuthCallback } from \"@advcomm/uids-io-auth-react\";\n\nexport function AuthCallbackPage() {\n  const navigate = useNavigate();\n  const { isProcessing, error } = useAuthCallback({\n    onSuccess: () => navigate(\"/dashboard\", { replace: true }),\n  });\n\n  if (error) return <p>{error.message}</p>;\n  return <p>{isProcessing ? \"Signing in…\" : \"Redirecting…\"}</p>;\n}\n```\n\n`useAuthCallback`:\n\n- Strips `?code` / `?state` from the URL synchronously (before any `await`)\n- Claims the OAuth redirect transaction once → `POST /token` with PKCE verifier and `X-Uids-Device-Id`\n- Replays safely on Strict Mode remount (no second token exchange)\n\nAdvanced: call `client.handleCallback(params)` directly only if you also use `clearOAuthRedirectFromUrl()` and understand the [redirect transaction](./docs/OAUTH_REDIRECT_PLAN.md).\n\nOAuth errors in the query string (`?error=...`) throw `OAuthError`.\n\n### 4. Sign-in and sign-out\n\n```tsx\nconst { signIn, signOut, isAuthenticated, isLoading, user, error } = useAuth();\n\n// Redirects to auth server /authorize → /login (Google, Microsoft, email, etc.)\nawait signIn();\n\n// Optional custom OAuth state\nawait signIn({ state: \"checkout\" });\n\n// POST /logout (HttpOnly refresh cookie or body refresh_token); clears local tokens\nawait signOut();\n```\n\nLogin UI lives on the **auth server** — the SDK does not embed provider secrets.\n\n### 5. Protect routes\n\n```tsx\nimport { useRequireAuth } from \"@advcomm/uids-io-auth-react\";\n\nexport function DashboardPage() {\n  useRequireAuth(); // redirects to signIn when not authenticated\n  // ...\n}\n```\n\nOr check `isAuthenticated` / `isLoading` manually for custom UX.\n\n### 6. Call your API\n\n```ts\nimport { createAuthFetch, useAuth } from \"@advcomm/uids-io-auth-react\";\n\nconst { client } = useAuth();\n\nconst apiFetch = createAuthFetch(\n  () => client.getAccessToken(),\n  () => client.refresh(),\n  {\n    onUnauthorized: () => {\n      void client.signIn();\n    },\n  },\n);\n\nconst res = await apiFetch(\"https://api.example.com/me\");\n```\n\n- Attaches `Authorization: Bearer <access_token>`\n- On **401**, refreshes once and retries\n- `getAccessToken()` refreshes proactively when the access token is near expiry\n\nYour API must use `requireAuth` with the same `issuer` and `audience` (`API_AUDIENCE` on the auth kit) as the auth server.\n\n### 7. Framework-agnostic client\n\nUse `createAuthClient` without React when needed:\n\n```ts\nimport { createAuthClient } from \"@advcomm/uids-io-auth-react\";\n\nconst client = createAuthClient(authConfig);\nawait client.registerDevice();\nawait client.signIn(); // full-page redirect\n// on callback page:\nawait client.handleCallback(new URLSearchParams(window.location.search));\nconst token = await client.getAccessToken();\n```\n\n---\n\n## Auth server endpoints used\n\n| Method | Path | SDK usage |\n|--------|------|-----------|\n| GET | `/.well-known/openid-configuration` | Optional config bootstrap |\n| GET | `/.well-known/oauth-providers` | `loadProviders()` |\n| POST | `/devices/register` | On first `signIn()` |\n| GET | `/authorize` | `signIn()` redirect (PKCE + `device_id`) |\n| POST | `/token` | `handleCallback()` |\n| POST | `/refresh` | `refresh()` / scheduled refresh |\n| POST | `/logout` | `signOut()` (refresh cookie or `{ refresh_token }`) |\n| GET | `/devices` | `listDevices()` |\n| POST | `/devices/revoke` | `revokeDevice()` |\n\n---\n\n## Local development\n\n### 1. Auth server (sibling repo)\n\nFrom [`docs/auth`](../auth):\n\n```bash\ncd ../auth\ncp examples/express-auth-server/.env.example examples/express-auth-server/.env\n# set DATABASE_URL, ISSUER=http://localhost:3000, API_AUDIENCE=http://localhost:4000, CSRF_SECRET=...\nnpm install\nnpm run build\nnpx tsx examples/express-auth-server/index.ts\n```\n\nListens on **http://localhost:3000** by default. Seeds OAuth clients for local development (see the [example app README](./examples/vite-example-portal/README.md)).\n\n### 2. API server (optional, for `/me` demo)\n\n```bash\nnpx tsx examples/express-api-server/index.ts\n```\n\nListens on **http://localhost:4000**. All routes require Bearer tokens except you call `/me` with a valid access token.\n\n### 3. SDK + example app\n\n```bash\ncd /path/to/sdk_react\nnpm install\nnpm run build\n\ncd examples/vite-example-portal\ncp .env.example .env\nnpm install\nnpm run dev\n```\n\nOr from repo root:\n\n```bash\nnpm run example:portal\n```\n\nOpen http://localhost:5173 → **Sign in** → complete login on auth server → dashboard → **GET /api/me** (Vite proxies `/api` → `localhost:4000`).\n\n---\n\n## End-to-end test checklist\n\nRun these with React **Strict Mode enabled** (default in the example app) and the Network tab open.\n\n| # | Flow | Expected |\n|---|------|----------|\n| 1 | **Cold load (signed out)** | At most one `POST /refresh` if a prior cookie session exists; no duplicate `openid-configuration` |\n| 2 | **Login page** | One `GET /.well-known/oauth-providers` when `loadProviders()` runs |\n| 3 | **Google sign-in** | Redirect to `{issuer}/authorize?...`; callback hits `{issuer}/token` **once** (no `invalid_grant` reuse) |\n| 4 | **Callback URL** | `?code` stripped from address bar before exchange completes |\n| 5 | **Dashboard** | `useRequireAuth` does not loop; user claims visible from ID token |\n| 6 | **API call** | `GET /api/me` returns 200 with Bearer token |\n| 7 | **Page refresh (F5)** | Session restored via silent `POST /refresh`; still authenticated |\n| 8 | **Sign out** | `POST /logout` returns 200 (no `csrf_failed` with cookie delivery + updated auth server) |\n| 9 | **After logout** | Protected routes redirect to sign-in; `/me` returns 401 without manual token |\n| 10 | **Re-login** | Full OAuth flow works again after sign-out |\n\n**Cross-origin local dev (`localhost:5173` → `localhost:3000`):** cookie refresh may not persist across reload unless you proxy auth under the app origin or use `VITE_AUTH_TOKEN_DELIVERY=body` in `.env`. See [TOKEN_STORAGE.md](./docs/TOKEN_STORAGE.md).\n\n---\n\n## Security defaults\n\n| Topic | Behavior |\n|-------|----------|\n| `device_id` | SDK-generated UUID v4 — **no browser fingerprinting** |\n| Access token | **Memory only** (short-lived) |\n| Refresh token (web) | **HttpOnly cookie** on auth issuer — not readable from JS |\n| Multi-tab | Refresh leader (`navigator.locks`) + `BroadcastChannel` |\n| `tokenDelivery: \"body\"` | JSON refresh token in storage (local dev / Flutter web) |\n| PKCE verifier | `sessionStorage` until callback, then deleted |\n\nFull detail: [docs/TOKEN_STORAGE.md](./docs/TOKEN_STORAGE.md).\n\n---\n\n## Errors\n\n| Class | When |\n|-------|------|\n| `OAuthError` | `{ error, error_description }` from auth server |\n| `ValidationError` | HTTP 422 with `details[]` |\n| `TokenReuseError` | `invalid_grant` with reuse/revoked message — tokens cleared |\n| `AuthSdkError` | Other failures |\n\n```ts\nimport { isOAuthError, isTokenReuseError } from \"@advcomm/uids-io-auth-react\";\n\ntry {\n  await client.refresh();\n} catch (e) {\n  if (isTokenReuseError(e)) {\n    await client.signIn();\n  }\n}\n```\n\n---\n\n## API reference\n\nFull JSDoc (parameters, throws, examples) is on the TypeScript types — open `src/auth-client/` or hover `createAuthClient` in your IDE after `npm run build`.\n\n### `createAuthClient(config)` → `AuthClient`\n\n| Method | Description |\n|--------|-------------|\n| `getDeviceId()` | UUID from storage (creates if missing) |\n| `registerDevice()` | `POST /devices/register` |\n| `signIn(options?)` | PKCE + redirect to `/authorize` |\n| `handleCallback(urlOrParams)` | Exchange code, store tokens |\n| `refresh()` | Rotate refresh token |\n| `getAccessToken()` | Returns token; refreshes if near expiry |\n| `signOut()` | Logout + clear local state |\n| `listDevices()` | Authenticated device list |\n| `revokeDevice(deviceId)` | Revoke a device |\n| `onTokensChanged(cb)` | Subscribe; called immediately with current tokens |\n| `loadProviders()` | Fetch and cache `GET /.well-known/oauth-providers` |\n| `initialize()` | Restore session via silent refresh on load |\n\n### React exports\n\n| Export | Role |\n|--------|------|\n| `AuthProvider` | Owns `AuthClient`, session bootstrap, token subscription |\n| `useAuth()` | `isAuthenticated`, `user`, `loadProviders`, `signIn`, `signOut`, `client`, `error` |\n| `useAuthCallback()` | Canonical OAuth callback route hook (Strict Mode safe) |\n| `useRequireAuth()` | Auto `signIn()` when unauthenticated after load |\n| `createAuthFetch()` | Bearer + 401 retry for resource APIs |\n| `getOrCreateAuthClient()` | Stable client instance across React remounts |\n\n### Advanced\n\n- `buildAuthorizeUrl`, `generatePkcePair` — custom redirects or tests\n- `OAuthError`, `TokenReuseError`, `ValidationError` — typed errors from auth HTTP\n\n---\n\n## Package development\n\n```bash\nnpm install      # installs Husky git hooks (prepare)\nnpm run build\nnpm test\nnpm run typecheck\nnpm run check    # biome (format + lint + organize imports)\nnpm run validate # check:ci + typecheck + test + build (same as pre-push hook)\n```\n\n**Git hooks (Husky):** `pre-commit` runs Biome on staged files; `pre-push` runs `npm run validate`. Skip with `git commit --no-verify` / `git push --no-verify`, or `HUSKY=0` (CI release job already sets this).\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---\n\n## Roadmap\n\n| Phase | Status |\n|-------|--------|\n| Phase 1 — Core client + React hooks | Shipped in this repo |\n| Phase 2 — Multi-tab refresh leader, app presets | Planned |\n| Phase 3 — `@advcomm/uids-io-auth-react/next` | Planned |\n\nDetails: [REACT_SDK_PLAN.md](../auth/docs/REACT_SDK_PLAN.md)\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-050d4f2fcbccd37b4ab4d2a404957744"}