{"_id":"@appdirect/spa-oauth-pkce","name":"@appdirect/spa-oauth-pkce","dist-tags":{"main":"0.0.1-main-4e5590b","latest":"0.0.1-main-4e5590b"},"versions":{"0.0.1-main-4e5590b":{"name":"@appdirect/spa-oauth-pkce","version":"0.0.1-main-4e5590b","description":"Framework-agnostic OAuth 2.0 Authorization Code + PKCE client for browser SPAs","engines":{"node":">=18"},"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"}}},"sideEffects":false,"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run --coverage","test:watch":"vitest","typecheck":"tsc --noEmit"},"keywords":["oauth","oauth2","pkce","oidc","spa","browser","authentication"],"license":"MIT","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"devDependencies":{"@vitest/coverage-v8":"^3.0.0","happy-dom":"^17.4.4","tsup":"^8.4.0","typescript":"~5.7.0","vitest":"^3.0.0"},"_id":"@appdirect/spa-oauth-pkce@0.0.1-main-4e5590b","gitHead":"4e5590b59ef5ee32edff9f5c78b8fd8d2a31a1e7","_nodeVersion":"18.20.8","_npmVersion":"10.8.2","dist":{"integrity":"sha512-sa2YJ2kKIhOc0SapgBTN07B4s8BEIH0el6NYm9qkPIvbSQxj5orNXXNs8t4YeiGk5VhGnN76K2iYLGlvVtOpYA==","shasum":"ef7bf6a8686ab0cf957944fc820a3b3768f746af","tarball":"https://registry.npmjs.org/@appdirect/spa-oauth-pkce/-/spa-oauth-pkce-0.0.1-main-4e5590b.tgz","fileCount":17,"unpackedSize":327096,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCE/6bQ/TLnzM2foeJQBdrpAaRkKcZLxU/QR7kcD/ZIVQIgMR2SRAdE7HkFjSUJC4ZBKHX6V+bTmYxoht16R1ABGkA="}]},"_npmUser":{"name":"thecynicaldev","email":"tarik.abou-saddik@appdirect.com"},"directories":{},"maintainers":[{"name":"posabsolute","email":"cedric.dugas@gmail.com"},{"name":"drodriguez94","email":"david.rodriguez@appdirect.com"},{"name":"engineering.procurement","email":"engineering.procurement@appdirect.com"},{"name":"thecynicaldev","email":"tarik.abou-saddik@appdirect.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/spa-oauth-pkce_0.0.1-main-4e5590b_1782831363804_0.5934508474520102"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-30T14:56:03.574Z","0.0.1-main-4e5590b":"2026-06-30T14:56:03.994Z","modified":"2026-06-30T14:56:04.373Z"},"maintainers":[{"name":"posabsolute","email":"cedric.dugas@gmail.com"},{"name":"drodriguez94","email":"david.rodriguez@appdirect.com"},{"name":"engineering.procurement","email":"engineering.procurement@appdirect.com"},{"name":"thecynicaldev","email":"tarik.abou-saddik@appdirect.com"}],"description":"Framework-agnostic OAuth 2.0 Authorization Code + PKCE client for browser SPAs","keywords":["oauth","oauth2","pkce","oidc","spa","browser","authentication"],"license":"MIT","readme":"# @appdirect/spa-oauth-pkce\n\nFramework-agnostic OAuth 2.0 Authorization Code + PKCE client for browser SPAs. No BFF required.\n\nDesigned for **AppDirect marketplaces** — supply issuer and OAuth endpoints; the SDK handles `/auth/token` and `/auth/refresh` automatically.\n\n## Features\n\n- OAuth 2.0 Authorization Code + PKCE (S256)\n- OIDC ID token validation (issuer, audience, expiry, nonce)\n- Explicit provider configuration (no browser-side OIDC discovery)\n- Built-in AppDirect secondary exchange (`/auth/token`) and session refresh (`/auth/refresh`)\n- Memory-first token storage with optional sessionStorage persistence\n- CORS-safe token requests via XMLHttpRequest\n- Event-driven `AuthManager` API — works with React, Vue, Angular, Svelte, or vanilla JS\n- Zero runtime dependencies\n\n## Install\n\nPublished to the [public npm registry](https://www.npmjs.com/package/@appdirect/spa-oauth-pkce)\n\n```bash\nnpm install @appdirect/spa-oauth-pkce\n```\n\n## Quick start\n\n```typescript\nimport { createAuthManager, createOAuthConfig } from \"@appdirect/spa-oauth-pkce\";\n\nconst config = createOAuthConfig({\n  clientId: \"your-client-id\",\n  provider: {\n    issuer: \"https://your-provider.example.com\",\n    authorizationEndpoint: \"https://your-provider.example.com/oauth2/authorize\",\n    tokenEndpoint: \"https://your-provider.example.com/oauth2/token\",\n  },\n  persistTokens: true,\n});\n\nconst auth = createAuthManager(config);\n\nawait auth.init(); // call on every page load\n\nauth.subscribe((state) => {\n  if (state.status === \"authenticated\") {\n    console.log(\"Logged in as\", state.user?.email);\n  }\n});\n\nauth.login();  // redirect to provider\nauth.logout(); // clear session\n```\n\n## Documentation\n\n\n| Guide                                    | Description                                                     |\n| ---------------------------------------- | --------------------------------------------------------------- |\n| **[Usage Guide](docs/USAGE.md)**         | Configuration, API reference, framework integration             |\n| **[Architecture](docs/ARCHITECTURE.md)** | Module layout, storage model, two-token model, extension points |\n| **[Flow](docs/FLOW.md)**                 | PKCE sequence diagrams, state machine, security model           |\n\n\n## Architecture\n\nSee **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** for the full module layout, storage model, and extension points.\n\n```\n┌─────────────────────────────────────────────────┐\n│  Your SPA (any framework)                       │\n│    auth.subscribe() → update UI                 │\n│    auth.login() / logout() / getAccessToken()   │\n└────────────────────┬────────────────────────────┘\n                     │\n┌────────────────────▼────────────────────────────┐\n│  AuthManager                                    │\n│    init() · login() · logout() · refresh()      │\n│    /auth/token · /auth/refresh · autoRefresh    │\n└────────────────────┬────────────────────────────┘\n                     │\n┌────────────────────▼────────────────────────────┐\n│  OAuthClient                                    │\n│    authorize() · handleCallback() · refresh()   │\n└──┬─────────┬─────────┬─────────┬───────────────┘\n   │         │         │         │\n  pkce      jwt       xhr    token-storage\n                              + auth-refresh\n```\n\n**Two API tiers:**\n\n\n| Tier                  | Use when                                      |\n| --------------------- | --------------------------------------------- |\n| `createAuthManager()` | Most apps — lifecycle + `subscribe()`         |\n| `OAuthClient`         | Full control over authorize/callback/refresh  |\n| `createOAuthConfig()` | Build config from explicit provider endpoints |\n\n\n## Provider configuration\n\n```typescript\nconst config = createOAuthConfig({\n  clientId: \"my-app\",\n  provider: {\n    issuer: \"https://your-provider.example.com\",\n    authorizationEndpoint: \"https://your-provider.example.com/oauth2/authorize\",\n    tokenEndpoint: \"https://your-provider.example.com/oauth2/token\",\n  },\n});\n```\n\nSee [Usage Guide → Provider configuration](docs/USAGE.md#provider-configuration) for JSON import and `tokenEndpointAuthMethod`.\n\n## AppDirect token flow\n\n`createAuthManager()` always performs the AppDirect two-step token model:\n\n1. After OAuth2 callback → POST `{issuer}/auth/token` with OAuth2 access token as Bearer\n2. Session refresh → POST `{issuer}/auth/refresh` with session refresh token as Bearer\n\nNo path configuration is required from consumer apps. Override only with a custom `onTokensReceived` hook if needed.\n\n```typescript\nconst auth = createAuthManager(config);\n```\n\nThe session refresh token is stored in `oauth_token_set.refreshToken` (single source of truth). `authRefreshTokenStorage` is a facade over that field; `AuthSnapshot.authRefreshToken` mirrors it.\n\n\n| Token                 | Storage                                   | After reload                         |\n| --------------------- | ----------------------------------------- | ------------------------------------ |\n| Session access token  | `oauth_token_set.accessToken`             | Restored when `persistTokens: true`  |\n| Session refresh token | `oauth_token_set.refreshToken`            | Restored when `persistTokens: true`  |\n| OAuth2 access token   | `AuthSnapshot.oauth2AccessToken` (memory) | `null`                               |\n| User profile          | `AuthSnapshot.user`                       | `null` — app loads via bootstrap API |\n\n\nSession restore is access-token-based: it checks expiry and refreshes via `/auth/refresh` when needed; user profile is not loaded on restore (`user` is `null` until the app provides it).\n\nSee [Usage Guide → Secondary token exchange](docs/USAGE.md#secondary-token-exchange).\n\n## CORS requirements\n\nThe token endpoint must allow cross-origin requests from your SPA origin. See [Usage Guide → CORS requirements](docs/USAGE.md#cors-requirements).\n\n## Security\n\n- Public client only — no `client_secret` in browser code\n- PKCE S256, state (CSRF), nonce (replay) validation on callback\n- Session restore is access-token-based — no ID token re-validation on reload\n- User profile after restore is the app's responsibility\n- Session tokens in `oauth_token_set` only — no separate refresh or OAuth2 storage keys\n- OAuth2 access token in memory after callback only — never persisted\n- Memory-first tokens; never uses `localStorage`\n- Callback URL cleaned from browser history\n\n## License\n\nMIT","readmeFilename":"README.md","_rev":"1-f1d1f9019cb6cf2f46ff798e0b724db1"}