{"_id":"@edwinlovett/ignite-auth","_rev":"3-eb48c253b3d22893a963a6addd0d1072","name":"@edwinlovett/ignite-auth","dist-tags":{"latest":"1.4.0"},"versions":{"1.0.0":{"name":"@edwinlovett/ignite-auth","version":"1.0.0","license":"UNLICENSED","_id":"@edwinlovett/ignite-auth@1.0.0","maintainers":[{"name":"edwinlovett","email":"edwin@edwinlovett.com"}],"homepage":"https://github.com/edwinlov3tt/ignite-workspace/tree/main/packages/ignite-auth","bugs":{"url":"https://github.com/edwinlov3tt/ignite-workspace/issues"},"dist":{"shasum":"00df2bcebb7884cc2a666e77a868b43b1a0bc308","tarball":"https://registry.npmjs.org/@edwinlovett/ignite-auth/-/ignite-auth-1.0.0.tgz","fileCount":14,"integrity":"sha512-zPnUkimpR6wzP3XR4vCnPOecNQ6uzfATGfJYrPKe8/JA2/K26yDgShnTT/ERdYL1RC9W0QlBqF4pdRtUljBM9Q==","signatures":[{"sig":"MEUCICAs2EAnL2IyVyhq8XtI8DIDLR6sZSm1r1pNtYcKmHpzAiEAxn9Op3mBrxC9rrSFEzfyoF3lo74gnevr4qNw4IAbzOY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":121442},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/react/index.cjs"}},"gitHead":"b45916e60524b54a2fe5d6e9bc24d0ccc5c236d7","scripts":{"test":"vitest run","build":"tsup","clean":"rm -rf dist .turbo","typecheck":"tsc --noEmit","prepublishOnly":"pnpm build"},"_npmUser":{"name":"edwinlovett","email":"edwin@edwinlovett.com"},"repository":{"url":"git+https://github.com/edwinlov3tt/ignite-workspace.git","type":"git","directory":"packages/ignite-auth"},"_npmVersion":"10.9.4","description":"Consumer SDK for the Ignite Toolbox unified worker — identity + brand context. Core is framework-agnostic; React hooks are an opt-in /react subpath.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^3.23.8","tsup":"^8.5.1","react":"^18.3.1","vitest":"2.1.8","typescript":"^5.7.2","@types/react":"^18.3.17"},"peerDependencies":{"zod":"^3.0.0","react":">=18.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ignite-auth_1.0.0_1779725896982_0.7140483203178887","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@edwinlovett/ignite-auth","version":"1.1.0","license":"UNLICENSED","_id":"@edwinlovett/ignite-auth@1.1.0","maintainers":[{"name":"edwinlovett","email":"edwin@edwinlovett.com"}],"homepage":"https://github.com/edwinlov3tt/ignite-workspace/tree/main/packages/ignite-auth","bugs":{"url":"https://github.com/edwinlov3tt/ignite-workspace/issues"},"dist":{"shasum":"746ae3db95d625dff9855fe44cc62c857e3fa933","tarball":"https://registry.npmjs.org/@edwinlovett/ignite-auth/-/ignite-auth-1.1.0.tgz","fileCount":30,"integrity":"sha512-Bwl6AbiOvkC1+fyT4C6NwHLpPAZoagopGxy4aw9eFyKgrxGwRi9KtQTqFSGfhLCHMSYwj/c/HfEp7H378DCnDA==","signatures":[{"sig":"MEQCICDLmHmhpvOcZ5DeSsgHiWBWKKc52WWnF4QgpWVcCabnAiAcQWLEeNf6L2qaLYcsyZOB1Qgm4dslsfDDMAFO1pXEmA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":259877},"main":"./dist/index.cjs","type":"module","_from":"file:edwinlovett-ignite-auth-1.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./sso":{"types":"./dist/sso/index.d.ts","import":"./dist/sso/index.js","require":"./dist/sso/index.cjs"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/react/index.cjs"},"./verify":{"types":"./dist/verify/index.d.ts","import":"./dist/verify/index.js","require":"./dist/verify/index.cjs"}},"scripts":{"test":"vitest run","build":"tsup","clean":"rm -rf dist .turbo","typecheck":"tsc --noEmit"},"_npmUser":{"name":"edwinlovett","email":"edwin@edwinlovett.com"},"_resolved":"/private/var/folders/nk/2ypypzvx1hg7q9rmk9ddc6th0000gn/T/db2f4aa7cf9e73fc72d3506576cb4574/edwinlovett-ignite-auth-1.1.0.tgz","_integrity":"sha512-Bwl6AbiOvkC1+fyT4C6NwHLpPAZoagopGxy4aw9eFyKgrxGwRi9KtQTqFSGfhLCHMSYwj/c/HfEp7H378DCnDA==","repository":{"url":"git+https://github.com/edwinlov3tt/ignite-workspace.git","type":"git","directory":"packages/ignite-auth"},"_npmVersion":"10.9.4","description":"Consumer SDK for the Ignite Toolbox unified worker — identity + brand context. Core is framework-agnostic; React hooks are an opt-in /react subpath.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.0","dependencies":{"jose":"^6.2.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^3.23.8","tsup":"^8.5.1","react":"^18.3.1","vitest":"2.1.8","typescript":"^5.7.2","@types/react":"^18.3.17"},"peerDependencies":{"zod":"^3.0.0","react":">=18.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ignite-auth_1.1.0_1783885797842_0.4034318810402058","host":"s3://npm-registry-packages-npm-production"}},"1.4.0":{"name":"@edwinlovett/ignite-auth","version":"1.4.0","description":"Consumer SDK for the Ignite Toolbox unified worker — identity + brand context. Core is framework-agnostic; React hooks are an opt-in /react subpath.","license":"UNLICENSED","homepage":"https://github.com/edwinlov3tt/ignite-workspace/tree/main/packages/ignite-auth","repository":{"type":"git","url":"git+https://github.com/edwinlov3tt/ignite-workspace.git","directory":"packages/ignite-auth"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/react/index.cjs"},"./sso":{"types":"./dist/sso/index.d.ts","import":"./dist/sso/index.js","require":"./dist/sso/index.cjs"},"./verify":{"types":"./dist/verify/index.d.ts","import":"./dist/verify/index.js","require":"./dist/verify/index.cjs"}},"sideEffects":false,"publishConfig":{"access":"public"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"vitest run","prepublishOnly":"pnpm build","clean":"rm -rf dist .turbo"},"dependencies":{"jose":"^6.2.3"},"peerDependencies":{"react":">=18.0.0","zod":"^3.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"devDependencies":{"@types/react":"^18.3.17","react":"^18.3.1","tsup":"^8.5.1","typescript":"^5.7.2","vitest":"2.1.8","zod":"^3.23.8"},"_id":"@edwinlovett/ignite-auth@1.4.0","bugs":{"url":"https://github.com/edwinlov3tt/ignite-workspace/issues"},"_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-h1ut8swSZnTPjjnchTMaka45LNg8d0MsXIXrnVvhu5JZQWaaGm7t0EM7KHSc8vvQuwsjv/EK6AZo5xCGwBElqQ==","shasum":"aad29343867b49e57de1919cbafe9168f5d48821","tarball":"https://registry.npmjs.org/@edwinlovett/ignite-auth/-/ignite-auth-1.4.0.tgz","fileCount":30,"unpackedSize":359702,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICcEkRdB05pHAtaHt8RjTUiknA/IelMjRzYdnqOctjd9AiEA8mz5epiyV2QD4Ica3r+j+nDtYjxyt5FkwIkXOJEuQgE="}]},"_npmUser":{"name":"edwinlovett","email":"edwin@edwinlovett.com"},"directories":{},"maintainers":[{"name":"edwinlovett","email":"edwin@edwinlovett.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ignite-auth_1.4.0_1784076248866_0.037371908432559664"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-25T16:18:16.849Z","modified":"2026-07-15T00:44:09.201Z","1.0.0":"2026-05-25T16:18:17.134Z","1.1.0":"2026-07-12T19:49:57.966Z","1.4.0":"2026-07-15T00:44:09.087Z"},"bugs":{"url":"https://github.com/edwinlov3tt/ignite-workspace/issues"},"license":"UNLICENSED","homepage":"https://github.com/edwinlov3tt/ignite-workspace/tree/main/packages/ignite-auth","repository":{"type":"git","url":"git+https://github.com/edwinlov3tt/ignite-workspace.git","directory":"packages/ignite-auth"},"description":"Consumer SDK for the Ignite Toolbox unified worker — identity + brand context. Core is framework-agnostic; React hooks are an opt-in /react subpath.","maintainers":[{"name":"edwinlovett","email":"edwin@edwinlovett.com"}],"readme":"# @edwinlovett/ignite-auth\n\nConsumer SDK for the [Ignite Toolbox](https://api.ignitetoolbelt.com) unified worker — identity + brand context.\n\nThe core client is framework-agnostic. React hooks ship from a separate\n`@edwinlovett/ignite-auth/react` subpath so non-React consumers don't pay the cost\nof pulling React into their bundle.\n\n> **Renamed from `@lovett/auth@0.4.0`.** Hook names are preserved 1:1 —\n> the workspace migration is import-path-only. The only behavioural\n> changes are the default `gateway` origin (`https://api.ignitetoolbelt.com`)\n> and the cookie name (`ignite_session` / `ignite_refresh`). See\n> [ADR-055](https://github.com/edwinlov3tt/ignite-workspace/blob/main/docs/adr/ADR-055-unified-ignite-toolbox-worker.md)\n> §D4.\n\n## Install\n\n```bash\nnpm install @edwinlovett/ignite-auth\n```\n\n## Quick start (vanilla)\n\n```ts\nimport { createAuthClient } from \"@edwinlovett/ignite-auth\";\n\nconst auth = createAuthClient({\n  // Optional — defaults to https://api.ignitetoolbelt.com.\n  // Override during the C7 cutover window if you're pointing at a\n  // preview deployment.\n  gateway: \"https://api.ignitetoolbelt.com\",\n});\n\nconst session = await auth.getSession();\nif (!session) {\n  auth.redirectToLogin({ redirect: window.location.href });\n} else {\n  console.log(\"signed in as\", session.user.email);\n}\n\n// Subscribe to changes (token refresh, sign-out, etc.)\nconst unsubscribe = auth.subscribe((s) => console.log(\"session →\", s));\n\n// Force refresh on demand\nawait auth.refresh();\n\n// Sign out\nawait auth.signOut();\n```\n\n## React\n\n```tsx\nimport { AuthProvider, useSession, useUser } from \"@edwinlovett/ignite-auth/react\";\nimport { createAuthClient } from \"@edwinlovett/ignite-auth\";\n\nconst auth = createAuthClient({ gateway: \"https://api.ignitetoolbelt.com\" });\n\nexport function App() {\n  return (\n    <AuthProvider client={auth}>\n      <Inner />\n    </AuthProvider>\n  );\n}\n\nfunction Inner() {\n  const { user, loading, signOut } = useSession();\n\n  if (loading) return null;\n  if (!user) return <a href=\"/login\">Sign in</a>;\n  return (\n    <div>\n      Hello, {user.email}{\" \"}\n      <button onClick={() => signOut()}>Sign out</button>\n    </div>\n  );\n}\n```\n\n`AuthProvider` puts the client on context. `useSession` exposes\n`user`, `expiresAt`, `loading`, `signOut`. `useUser` is shorthand for\n`useSession().user`.\n\n### Brand-aware hooks (ADR-041)\n\n```tsx\nimport {\n  useActiveBrand,\n  useAvailableBrands,\n  useAvailableGroups,\n  useTaxonomies,\n} from \"@edwinlovett/ignite-auth/react\";\n\nfunction BrandPicker({ slug }: { slug?: string }) {\n  const brand = useActiveBrand(slug);     // current brand (URL-driven)\n  const brands = useAvailableBrands();    // every brand the user can reach\n  const groups = useAvailableGroups();    // picker grouping metadata\n  const taxonomies = useTaxonomies();     // tenant-scoped tag dictionary\n  // ...\n}\n```\n\nThe deprecated `useActiveClient` alias (ADR-041 D14) is also exported\nand emits a dev-only console warning — migrate to `useActiveBrand` at\nyour leisure.\n\n## API\n\n### `createAuthClient(opts) → AuthClient`\n\n| Option | Type | Default | |\n|---|---|---|---|\n| `gateway` | `string` | `https://api.ignitetoolbelt.com` | base origin of the unified worker; trailing slash stripped |\n| `fetch` | `typeof fetch` | global `fetch` | inject for tests/instrumentation |\n| `refreshLeadSeconds` | `number` | `60` | refresh this many seconds before token expiry |\n\n### `AuthClient`\n\n- `getSession() → Promise<Session | null>` — first call hits the\n  gateway; subsequent calls return cached value\n- `peekSession() → Session | null` — synchronous, may be null pre-load\n- `isAuthenticated() → boolean`\n- `subscribe(listener) → unsubscribe`\n- `redirectToLogin({ redirect? })` — full-page redirect to `/login`\n- `refresh() → Promise<Session | null>`\n- `signOut() → Promise<void>` — calls `/auth/logout`; the worker clears\n  the `ignite_session` / `ignite_refresh` cookies\n- `getAvailableBrands() → AvailableBrand[]`\n- `getAvailableGroups() → JwtBrandGroup[]`\n- `getActiveBrand() → AvailableBrand | null` — landing preference\n- `findBrandBySlug(slug) → AvailableBrand | null`\n- `switchBrand(brandId) → Promise<Session | null>`\n- `grantSelfBrandAccess(brandId, role?) → Promise<Session | null>`\n- `listTaxonomies() → Promise<TaxonomyWithValues[]>` — memoised per session\n\n## How it works\n\nThe SDK assumes a session cookie on the parent domain (e.g.\n`.ignitetoolbelt.com`). It calls `GET /auth/session` to read the current\nsession, attempts `POST /auth/refresh` once on a 401, and schedules a\nproactive refresh ~60s before expiry. There's no `localStorage` —\nHttpOnly cookies (`ignite_session` / `ignite_refresh`) are the source\nof truth; the SDK only mirrors them in memory.\n\n## Cross-origin requirements\n\nFor SSO across sibling subdomains the unified worker is configured with:\n\n- `COOKIE_DOMAIN=.ignitetoolbelt.com` (or your equivalent)\n- `ALLOWED_ORIGINS` including each consumer's exact origin\n\nThe SDK sends `credentials: 'include'` on every gateway call.\n\n---\n\n# Ecosystem apps: SSO + offline verification (ADR-134)\n\nTwo extra subpaths, for apps that are **not** the Ignite workspace. They're\nindependent: `/sso` is dependency-free and isomorphic (~1 KB); `/verify` is\nserver-only and the sole importer of `jose`. Import only what you need.\n\n## The login page (`/react`) — don't build your own\n\n`<IgniteLoginPage />` is the canonical Ignite sign-in screen. Render it and you get the\nSSO flow, the error semantics, and the layout — consistent with every other Ignite app.\n\n```tsx\nimport { IgniteLoginPage } from '@edwinlovett/ignite-auth/react'\n\n<IgniteLoginPage productName=\"Meta Ads Audit\" redirect={location.origin + '/dashboard'} />\n```\n\nNo CSS import, no design-system dependency. Theme it by mapping your tokens onto its CSS\ncustom properties:\n\n```tsx\n<IgniteLoginPage style={{ '--ia-accent': '#2563eb', '--ia-radius': '10px' } as CSSProperties} />\n```\n\n`--ia-accent`, `--ia-accent-fg`, `--ia-fg`, `--ia-muted`, `--ia-border`, `--ia-card`,\n`--ia-page`, `--ia-radius`, `--ia-danger`. Plus `className` (root) and per-part classes\n(`.ia-card`, `.ia-sso`, `.ia-input`, `.ia-submit`). Dark mode follows\n`prefers-color-scheme` unless you override.\n\n**The email field is hidden by default** — the passwordless path currently admits nobody\n(the corporate domains are SSO-only). `emailFallback` re-enables it when the client tier\nlands.\n\n## `@edwinlovett/ignite-auth/sso` — start the Microsoft sign-in flow\n\nSSO is a **full-page navigation** (the start route 302s to Microsoft), never a\nfetch. Build the URL here so your app can't drift from the worker's contract.\n\n```ts\nimport { ssoStartUrl } from '@edwinlovett/ignite-auth/sso'\n\nwindow.location.assign(\n  ssoStartUrl({ redirect: window.location.origin + '/dashboard' }),\n)\n```\n\n`redirect` must be an **absolute URL on a host the worker allows**\n(`ALLOWED_REDIRECT_HOSTS`) — pass `window.location.origin + path`, not a bare\npath. Pass `ext: true` from a Chrome-extension popup to land on a\n\"signed in, close this window\" page instead of redirecting.\n\nReact apps with no design system can use the batteries-included button:\n\n```tsx\nimport { SignInWithMicrosoft } from '@edwinlovett/ignite-auth/react'\n\n<SignInWithMicrosoft />                       // returns to the current page\n<SignInWithMicrosoft redirect=\"https://app.ignitetoolbelt.com/home\" />\n```\n\n## Signing out (`/sso`) — clearing your own cookie is NOT enough\n\nThe most common ecosystem-app bug: you clear *your* session cookie, but the shared\n`ignite_session` survives on `.ignitetoolbelt.com`, your next request verifies it, and the\nuser is **instantly signed back in**. Sign-out becomes a no-op that looks like it worked.\n\nYou also can't fix it by expiring `ignite_session` yourself — that drops the cookie from the\nbrowser but does **not revoke the session** in D1 (the refresh token stays live). Only the\ngateway can revoke.\n\n```ts\nimport { logoutUrl, logout } from '@edwinlovett/ignite-auth/sso'\n\n// Link / redirect (server-rendered friendly, no CORS needed):\n<a href={logoutUrl({ redirect: 'https://myapp.ignitetoolbelt.com/' })}>Sign out</a>\n\n// Or a credentialed POST, to stay on the page (SPA):\nawait logout()\n```\n\nBoth revoke the session server-side and clear `ignite_session` + `ignite_refresh`. Afterwards,\ndrop your own app cookie/state too.\n\n**Microsoft session:** `federated` defaults to `false` — the user leaves Ignite but stays\nsigned into their work Microsoft account (so \"Sign in with Microsoft\" re-auths with no\nprompt). Pass `logoutUrl({ federated: true })` to *also* end the Entra session — for shared\nmachines, or an explicit \"sign out everywhere\".\n\n## `@edwinlovett/ignite-auth/verify` — trust the cookie, server-side\n\nAny app on `*.ignitetoolbelt.com` **automatically receives** the `ignite_session`\ncookie (it's scoped to the registrable domain). This verifies it is real —\nsignature, algorithm, issuer, audience, expiry — against the gateway's public\nJWKS. **No secret, no per-request call to us.**\n\n```ts\nimport { createSessionVerifier } from '@edwinlovett/ignite-auth/verify'\n\nconst auth = createSessionVerifier()   // defaults to the prod gateway's JWKS\n\n// Cloudflare Workers / Hono\nconst session = await auth.verifyRequest(request)      // or c.req.raw\n// Express / Vercel\nconst session = await auth.verifyRequest(req)\n// → { userId, tenantId, orgId, role, sessionId, lastActiveBrandId, expiresAt }\n```\n\n`verify`/`verifyRequest` **throw** `SessionVerificationError` (with a `.reason`)\nso a forgotten `try/catch` fails closed. Use `tryVerify`/`tryVerifyRequest` for\na `null`-on-failure variant.\n\n`alg` is pinned to `EdDSA`, so `alg: none` and HS256-with-public-key confusion\nare rejected outright. The JWKS is fetched once and cached (and refetched on an\nunknown `kid`, so key rotation just works).\n\n### Two limits — read these\n\n1. **Revocation is invisible to an offline verifier.** The worker checks D1\n   `revoked_at` on every call and revokes instantly; this cannot. It will accept\n   a revoked-but-unexpired token for **up to the 15-minute access TTL**. That\n   short TTL is the bound. For sensitive or destructive operations, *also* make\n   an online check (`GET /auth/session`).\n2. **This is authentication, not authorization.** A valid token proves the bearer\n   is a signed-in Ignite user. It does **not** prove they may use *your* app — a\n   shared SSO cookie serves every app on the domain by design. Gate your app\n   separately.\n\n> **Not a security control:** publishing this package privately would gate who\n> can *download the code*, never who can *authenticate*. The SSO endpoints are\n> public browser redirects by necessity, and a browser SDK cannot hold a secret.\n> The real controls are the ones above: signature, `alg`, `iss`, `aud`, `exp`.\n\n## License\n\nUNLICENSED — distribution + use by Ignite Toolbox consumers only.\n","readmeFilename":"README.md"}