From ebc7343ebd4a7f8c377fe7d21b920856c49042e9 Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 11:00:00 -0700 Subject: [PATCH 01/14] feat: proactive silent token refresh via setInterval polling Adds interval-based polling to OidcClient that refreshes the access token before it expires, so consumers always have a valid token without needing 401 interceptors or RequireAuth re-mount flashes. Closes #74 Co-Authored-By: Claude Opus 4.6 --- packages/client/src/client.ts | 31 +++++ packages/client/src/tests/client.test.ts | 170 +++++++++++++++++++++++ packages/client/src/types.ts | 4 + 3 files changed, 205 insertions(+) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index b290331..242e743 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -13,6 +13,7 @@ import { parseUserinfoResponse, buildLogoutUrl, decodeJwtPayload, + isExpiredAt, DEFAULT_EXPIRY_BUFFER, type OidcDiscovery, type OidcUser, @@ -65,6 +66,7 @@ export class OidcClient { private subscribers = new Set(); private abortController: AbortController | null = null; private refreshPromise: Promise | null = null; + private autoRefreshTimer: ReturnType | null = null; private _state: AuthState = { user: null, @@ -178,6 +180,8 @@ export class OidcClient { isLoading: false, }); + this.startAutoRefresh(); + return { returnTo }; } @@ -228,6 +232,7 @@ export class OidcClient { * with the current ID token hint and `postLogoutRedirectUri` from config. */ logout(): void { + this.stopAutoRefresh(); const idToken = this._state.tokens.id; this.setState({ @@ -305,6 +310,8 @@ export class OidcClient { error: null, }); + this.startAutoRefresh(); + return newTokens; } @@ -331,10 +338,34 @@ export class OidcClient { * Tears down the client by aborting any in-flight requests and removing all subscribers. */ destroy(): void { + this.stopAutoRefresh(); this.abortController?.abort(); this.subscribers.clear(); } + private startAutoRefresh(): void { + if (this.config.autoRefresh === false) return; + if (this.autoRefreshTimer) return; + + const intervalSeconds = this.config.autoRefreshInterval ?? 10; + const buffer = this.config.expiryBuffer ?? DEFAULT_EXPIRY_BUFFER; + + this.autoRefreshTimer = setInterval(() => { + const { expiresAt } = this._state.tokens; + if (!this._state.isAuthenticated || !this._state.tokens.refresh) return; + if (!isExpiredAt(expiresAt, buffer)) return; + + this.refresh().catch(() => { this.stopAutoRefresh(); }); + }, intervalSeconds * 1000); + } + + private stopAutoRefresh(): void { + if (this.autoRefreshTimer) { + clearInterval(this.autoRefreshTimer); + this.autoRefreshTimer = null; + } + } + /** * Internal helper that calls the userinfo endpoint and parses the response. * diff --git a/packages/client/src/tests/client.test.ts b/packages/client/src/tests/client.test.ts index 18af59e..0197f55 100644 --- a/packages/client/src/tests/client.test.ts +++ b/packages/client/src/tests/client.test.ts @@ -330,4 +330,174 @@ describe("OidcClient", () => { expect(fn).not.toHaveBeenCalled(); }); }); + + describe("autoRefresh", () => { + function setupAuthenticatedClient(configOverrides?: Partial) { + Object.defineProperty(window, "location", { + value: { + href: "http://localhost:3000?code=auth_code&state=test-state", + search: "?code=auth_code&state=test-state", + pathname: "/", + hash: "", + }, + writable: true, + configurable: true, + }); + + sessionStorage.setItem( + "oidc-js:auth-state", + JSON.stringify({ + codeVerifier: "test-verifier", + state: "test-state", + nonce: "test-nonce", + redirectUri: "http://localhost:3000/callback", + }), + ); + + return new OidcClient({ ...CONFIG, fetchProfile: false, ...configOverrides }); + } + + it("calls refresh when token is near expiry", async () => { + const expiringSoonToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 10 }); + const tokenResponse = { ...TOKEN_RESPONSE, access_token: expiringSoonToken }; + mockFetchResponses(DISCOVERY, tokenResponse); + + const client = setupAuthenticatedClient({ autoRefreshInterval: 0.1, expiryBuffer: 30 }); + await client.init(); + + const freshToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 7200 }); + mockFetchResponses({ + access_token: freshToken, + token_type: "Bearer", + refresh_token: "rt_new", + }); + + await vi.waitFor(() => { + expect(client.state.tokens.access).toBe(freshToken); + }, { timeout: 1000 }); + + client.destroy(); + }); + + it("does not refresh when token is not near expiry", async () => { + mockFetchResponses(DISCOVERY, TOKEN_RESPONSE); + + const client = setupAuthenticatedClient({ autoRefreshInterval: 0.1 }); + await client.init(); + + const callCountAfterInit = fetchMock.mock.calls.length; + + await new Promise((r) => setTimeout(r, 300)); + + expect(fetchMock.mock.calls.length).toBe(callCountAfterInit); + + client.destroy(); + }); + + it("does not start when autoRefresh is false", async () => { + const expiringSoonToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 10 }); + const tokenResponse = { ...TOKEN_RESPONSE, access_token: expiringSoonToken }; + mockFetchResponses(DISCOVERY, tokenResponse); + + const client = setupAuthenticatedClient({ autoRefresh: false, autoRefreshInterval: 0.1, expiryBuffer: 30 }); + await client.init(); + + const callCountAfterInit = fetchMock.mock.calls.length; + + await new Promise((r) => setTimeout(r, 300)); + + expect(fetchMock.mock.calls.length).toBe(callCountAfterInit); + + client.destroy(); + }); + + it("stops on logout", async () => { + const expiringSoonToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 10 }); + const tokenResponse = { ...TOKEN_RESPONSE, access_token: expiringSoonToken }; + mockFetchResponses(DISCOVERY, tokenResponse); + + const client = setupAuthenticatedClient({ autoRefreshInterval: 0.1, expiryBuffer: 30 }); + await client.init(); + + client.logout(); + + const callCountAfterLogout = fetchMock.mock.calls.length; + + await new Promise((r) => setTimeout(r, 300)); + + expect(fetchMock.mock.calls.length).toBe(callCountAfterLogout); + }); + + it("stops polling after a failed refresh", async () => { + const expiringSoonToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 10 }); + const tokenResponse = { ...TOKEN_RESPONSE, access_token: expiringSoonToken }; + mockFetchResponses(DISCOVERY, tokenResponse); + + const client = setupAuthenticatedClient({ autoRefreshInterval: 0.1, expiryBuffer: 30 }); + await client.init(); + + fetchMock.mockRejectedValueOnce(new Error("network error")); + + await vi.waitFor(() => { + expect(fetchMock.mock.calls.length).toBeGreaterThan(2); + }, { timeout: 1000 }); + + const callCountAfterFailure = fetchMock.mock.calls.length; + + await new Promise((r) => setTimeout(r, 300)); + + expect(fetchMock.mock.calls.length).toBe(callCountAfterFailure); + expect(client.state.isAuthenticated).toBe(true); + + client.destroy(); + }); + + it("restarts after a failed auto-refresh followed by a manual refresh", async () => { + const expiringSoonToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 10 }); + const tokenResponse = { ...TOKEN_RESPONSE, access_token: expiringSoonToken }; + mockFetchResponses(DISCOVERY, tokenResponse); + + const client = setupAuthenticatedClient({ autoRefreshInterval: 0.1, expiryBuffer: 30 }); + await client.init(); + + // Auto-refresh fires and fails → interval stops + fetchMock.mockRejectedValueOnce(new Error("network error")); + + await vi.waitFor(() => { + expect(fetchMock.mock.calls.length).toBeGreaterThan(2); + }, { timeout: 1000 }); + + // Manual refresh succeeds → interval restarts + const freshToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 10 }); + mockFetchResponses({ access_token: freshToken, token_type: "Bearer", refresh_token: "rt_new" }); + await client.refresh(); + + // Auto-refresh should fire again with the new (still near-expiry) token + const secondFreshToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 7200 }); + mockFetchResponses({ access_token: secondFreshToken, token_type: "Bearer", refresh_token: "rt_new2" }); + + await vi.waitFor(() => { + expect(client.state.tokens.access).toBe(secondFreshToken); + }, { timeout: 1000 }); + + client.destroy(); + }); + + it("stops on destroy", async () => { + const expiringSoonToken = makeJwt({ sub: "user-1", exp: nowSeconds() + 10 }); + const tokenResponse = { ...TOKEN_RESPONSE, access_token: expiringSoonToken }; + mockFetchResponses(DISCOVERY, tokenResponse); + + const client = setupAuthenticatedClient({ autoRefreshInterval: 0.1, expiryBuffer: 30 }); + await client.init(); + + client.destroy(); + + const callCountAfterDestroy = fetchMock.mock.calls.length; + + await new Promise((r) => setTimeout(r, 300)); + + expect(fetchMock.mock.calls.length).toBe(callCountAfterDestroy); + }); + }); }); diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index a62ad0f..990046b 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -60,6 +60,10 @@ export interface LoginOptions { export interface OidcClientConfig extends OidcConfig { /** Whether to fetch the userinfo profile after token exchange. Defaults to true. */ fetchProfile?: boolean; + /** Whether to proactively refresh the access token before it expires. Defaults to true. */ + autoRefresh?: boolean; + /** Polling interval in seconds for the auto-refresh check. Defaults to 10. */ + autoRefreshInterval?: number; } /** From 09af0b08b3632b84bd02fe13fccffa36a4261c75 Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 11:00:59 -0700 Subject: [PATCH 02/14] docs: add proactive token refresh guide Co-Authored-By: Claude Opus 4.6 --- .../src/content/docs/guides/token-refresh.mdx | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/docs-web/src/content/docs/guides/token-refresh.mdx b/docs-web/src/content/docs/guides/token-refresh.mdx index 6edfbd2..b3b844a 100644 --- a/docs-web/src/content/docs/guides/token-refresh.mdx +++ b/docs-web/src/content/docs/guides/token-refresh.mdx @@ -25,9 +25,42 @@ sequenceDiagram OidcClient->>App: Render protected content ``` -## Automatic refresh with RequireAuth +## Proactive refresh -`RequireAuth` handles refresh automatically. When a user navigates to a protected route with an expired token: +By default, `OidcClient` proactively refreshes the access token *before* it expires using a short `setInterval` poll. Every tick compares the current time against `expiresAt - expiryBuffer` and triggers a refresh when the token is about to expire. This means the token is always fresh — no 401s, no interceptors needed, no fallback flash. + +This is enabled by default. You can configure the behavior with two options: + +```ts +const config = { + issuer: "https://auth.example.com", + clientId: "my-app", + redirectUri: "http://localhost:5173/callback", + expiryBuffer: 60, // refresh 60s before expiry (default: 30) + autoRefresh: true, // enabled by default + autoRefreshInterval: 10, // polling interval in seconds (default: 10) +}; +``` + +The polling interval is lightweight — one number comparison per tick, no re-renders or network calls until a refresh is actually needed. The approach is drift-proof and sleep-proof: after a laptop wakes from sleep, the very first tick catches the expiration. + + + +### Disabling proactive refresh + +Set `autoRefresh: false` to rely on reactive mechanisms instead (interceptors, `RequireAuth` re-mount): + +```tsx + + + +``` + +## Reactive refresh with RequireAuth + +`RequireAuth` handles refresh reactively. When a user navigates to a protected route with an expired token: 1. Checks `tokens.expiresAt` using `isExpiredAt()` from core (with an optional buffer) 2. If expired, calls `actions.refresh()` @@ -48,6 +81,10 @@ You can set an `expiryBuffer` (in seconds) on the provider config to refresh the ``` + + ## Manual refresh You can also trigger a refresh manually: From 433e70a716767670f17eef5198a95ff2702d1355 Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 11:14:56 -0700 Subject: [PATCH 03/14] docs: add decision 027 for proactive token refresh Co-Authored-By: Claude Opus 4.6 --- decisions.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/decisions.md b/decisions.md index 076e47c..ce8d0a7 100644 --- a/decisions.md +++ b/decisions.md @@ -315,3 +315,16 @@ Architectural and design decisions for the oidc-js project. Each entry captures **Decision**: Option 3. `trackTraffic(page)` now returns `{ requests(), navigations(), sequence() }`. The sequence log records fetch requests as `GET /path` or `POST /path` and navigations as `NAV /path`, in the order Playwright observes them. All three are asserted in tests that check traffic. **Rationale**: The combined sequence is the source of truth for protocol ordering. The separate `requests()` and `navigations()` remain as convenience filters for tests that only need to check "which fetches happened" without reasoning about navigation interleaving. The assumed race condition from decision 023 turned out to be incorrect. Playwright's `page.on("request")` fires deterministically for both fetch and document requests, and the combined log is stable across runs. Named constants (`GET_WELLKNOWN`, `POST_TOKEN`, `GET_USERINFO`, `NAV_AUTHORIZE`, `NAV_LOGOUT`) make the sequence assertions read like a protocol spec. + +### 027 - Proactive token refresh via setInterval polling (2026-05-08) + +**Context**: Token expiration was only detected reactively — by `RequireAuth` at render time or by HTTP interceptors catching 401s. This meant consumers using plain `fetch` had to manually check for 401 on every request, and `RequireAuth` briefly unmounted children when it detected expiration (losing form state). + +**Alternatives considered**: +1. Single `setTimeout` scheduled for `expiresAt - buffer` — simple but unreliable (browsers throttle background tabs, laptops sleep, timer fires late or not at all) +2. `setInterval` polling with a short interval (e.g. 10s) that checks `Date.now() >= expiresAt - expiryBuffer` each tick +3. Service Worker or `BroadcastChannel` based approach — complex, not universally supported + +**Decision**: Option 2. A `setInterval` in `OidcClient` polls every `autoRefreshInterval` seconds (default 10) and triggers `refresh()` when `isExpiredAt(expiresAt, expiryBuffer)` returns true. Enabled by default (`autoRefresh: true`). On refresh failure, the interval stops (no endpoint hammering); a successful manual refresh or new login restarts it. + +**Rationale**: This is the same approach `oidc-client-ts` uses (their `Timer` class polls every 5s). A short interval is drift-proof (always compares real time), sleep-proof (first tick after wake catches expiration), and cheap (one number comparison per tick). The implementation lives in `OidcClient`, so all framework adapters benefit via the existing subscribe/notify mechanism. The `expiryBuffer` config (already existed) controls how early the refresh fires — the token is still valid during the refresh request, so `RequireAuth` never sees an expired token and children stay mounted. From 2b6b922d1edcb0099bad12fbbed811f8fe0eb4da Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 16:11:49 -0700 Subject: [PATCH 04/14] fix: use OidcClientConfig in all framework adapters Adapters previously typed the config prop as OidcConfig (from core), which excluded client-specific options like autoRefresh and autoRefreshInterval. Changed to OidcClientConfig from oidc-js so TypeScript accepts the full config surface. Bumped all packages to 1.1.2. Co-Authored-By: Claude Opus 4.6 --- packages/angular/package.json | 2 +- packages/angular/src/index.ts | 1 + packages/angular/src/types.ts | 6 +++--- packages/client/package.json | 2 +- packages/kasper/package.json | 2 +- packages/kasper/src/auth-provider.ts | 4 ++-- packages/kasper/src/context.ts | 7 +++---- packages/kasper/src/index.ts | 1 + packages/kasper/src/types.ts | 5 ++--- packages/lit/package.json | 2 +- packages/lit/src/auth-controller.ts | 5 ++--- packages/lit/src/index.ts | 1 + packages/lit/src/types.ts | 6 ++---- packages/preact/package.json | 2 +- packages/preact/src/context.tsx | 5 ++--- packages/preact/src/index.ts | 1 + packages/preact/src/types.ts | 6 ++---- packages/react/package.json | 2 +- packages/react/src/context.tsx | 5 ++--- packages/react/src/index.ts | 1 + packages/react/src/types.ts | 6 ++---- packages/solid/package.json | 2 +- packages/solid/src/context.tsx | 5 ++--- packages/solid/src/index.ts | 1 + packages/solid/src/types.ts | 6 ++---- packages/svelte/package.json | 2 +- packages/svelte/src/AuthProvider.svelte | 4 ++-- packages/svelte/src/context.svelte.ts | 7 +++---- packages/svelte/src/index.ts | 1 + packages/svelte/src/types.ts | 6 ++---- packages/vue/package.json | 2 +- packages/vue/src/index.ts | 1 + packages/vue/src/plugin.ts | 5 ++--- packages/vue/src/types.ts | 8 +++----- 34 files changed, 55 insertions(+), 67 deletions(-) diff --git a/packages/angular/package.json b/packages/angular/package.json index 7222466..a4177c6 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "oidc-js-angular", - "version": "1.1.0", + "version": "1.1.2", "description": "Simple OIDC authentication for Angular. Signals, DI, and route guards with zero dependencies.", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index e49c228..3fe4964 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -10,4 +10,5 @@ export type { LoginOptions, } from "./types.js"; +export type { OidcClientConfig } from "oidc-js"; export type { OidcConfig, OidcUser, TokenSet } from "oidc-js-core"; diff --git a/packages/angular/src/types.ts b/packages/angular/src/types.ts index e899934..9f58f2f 100644 --- a/packages/angular/src/types.ts +++ b/packages/angular/src/types.ts @@ -1,7 +1,7 @@ -import type { OidcConfig } from "oidc-js-core"; - export type { IdTokenClaims, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; +import type { OidcClientConfig } from "oidc-js"; + /** * Configuration options for {@link provideAuth}. * @@ -10,7 +10,7 @@ export type { IdTokenClaims, AuthUser, AuthTokens, LoginOptions } from "oidc-js" */ export interface AuthProviderOptions { /** Core OIDC configuration (issuer, clientId, redirectUri, scopes, etc.). */ - config: OidcConfig; + config: OidcClientConfig; /** Whether to fetch the userinfo profile after token exchange. Defaults to `true`. */ fetchProfile?: boolean; /** diff --git a/packages/client/package.json b/packages/client/package.json index 89fd4fb..f8ecf2c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "oidc-js", - "version": "1.1.0", + "version": "1.1.2", "description": "Simple OIDC authentication for JavaScript. Drop-in client with login, logout, token refresh, and zero dependencies.", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/kasper/package.json b/packages/kasper/package.json index 3d03a95..113c801 100644 --- a/packages/kasper/package.json +++ b/packages/kasper/package.json @@ -1,6 +1,6 @@ { "name": "oidc-js-kasper", - "version": "1.1.0", + "version": "1.1.2", "description": "Simple OIDC authentication for Kasper.js. Signals, components, and auth guards with zero dependencies.", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/kasper/src/auth-provider.ts b/packages/kasper/src/auth-provider.ts index 8dd55b4..887a295 100644 --- a/packages/kasper/src/auth-provider.ts +++ b/packages/kasper/src/auth-provider.ts @@ -1,10 +1,10 @@ import { Component } from "kasper-js"; -import type { OidcConfig } from "oidc-js-core"; +import type { OidcClientConfig } from "oidc-js"; import type { OidcClient } from "oidc-js"; import { _initAuth, _destroyAuth } from "./context.js"; interface AuthProviderArgs { - config: OidcConfig; + config: OidcClientConfig; fetchProfile?: boolean; onLogin?: (returnTo: string) => void; onError?: (error: Error) => void; diff --git a/packages/kasper/src/context.ts b/packages/kasper/src/context.ts index ae4033d..44ccf06 100644 --- a/packages/kasper/src/context.ts +++ b/packages/kasper/src/context.ts @@ -1,6 +1,5 @@ import { signal, batch } from "kasper-js"; -import { OidcClient, type AuthState, type LoginOptions } from "oidc-js"; -import type { OidcConfig } from "oidc-js-core"; +import { OidcClient, type OidcClientConfig, type AuthState, type LoginOptions } from "oidc-js"; import type { AuthContextValue, AuthActions } from "./types.js"; const _user = signal(null); @@ -15,13 +14,13 @@ const _tokens = signal({ }); let _client: OidcClient | null = null; -let _config: OidcConfig | null = null; +let _config: OidcClientConfig | null = null; let _actions: AuthActions | null = null; let _unsub: (() => void) | null = null; /** @internal */ export function _initAuth( - config: OidcConfig, + config: OidcClientConfig, fetchProfile: boolean, ): { client: OidcClient; unsub: () => void } { const client = new OidcClient({ ...config, fetchProfile }); diff --git a/packages/kasper/src/index.ts b/packages/kasper/src/index.ts index 06bb566..7747cc2 100644 --- a/packages/kasper/src/index.ts +++ b/packages/kasper/src/index.ts @@ -10,4 +10,5 @@ export type { Signal, LoginOptions, } from "./types.js"; +export type { OidcClientConfig } from "oidc-js"; export type { OidcConfig, OidcUser, TokenSet } from "oidc-js-core"; diff --git a/packages/kasper/src/types.ts b/packages/kasper/src/types.ts index 49cebd4..a1a2e24 100644 --- a/packages/kasper/src/types.ts +++ b/packages/kasper/src/types.ts @@ -1,6 +1,5 @@ import type { Signal } from "kasper-js"; -import type { OidcConfig } from "oidc-js-core"; -import type { AuthUser, AuthTokens, LoginOptions } from "oidc-js"; +import type { OidcClientConfig, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; import type { OidcUser } from "oidc-js-core"; export type { IdTokenClaims, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; @@ -21,7 +20,7 @@ export interface AuthActions { /** Value returned by {@link useAuth}. All state properties are Kasper signals. */ export interface AuthContextValue { /** The OIDC configuration used to initialize the provider. */ - readonly config: OidcConfig; + readonly config: OidcClientConfig; /** The authenticated user, or null if not authenticated. */ readonly user: Signal; /** Whether the user is currently authenticated. */ diff --git a/packages/lit/package.json b/packages/lit/package.json index 32c8f9d..02241bb 100644 --- a/packages/lit/package.json +++ b/packages/lit/package.json @@ -1,6 +1,6 @@ { "name": "oidc-js-lit", - "version": "1.1.0", + "version": "1.1.2", "description": "Simple OIDC authentication for Lit. Reactive controllers for login, logout, and token refresh with zero dependencies.", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/lit/src/auth-controller.ts b/packages/lit/src/auth-controller.ts index b057224..431bbb2 100644 --- a/packages/lit/src/auth-controller.ts +++ b/packages/lit/src/auth-controller.ts @@ -1,6 +1,5 @@ import type { ReactiveController, ReactiveControllerHost } from "lit"; -import { OidcClient, type AuthState, type AuthUser, type AuthTokens, type LoginOptions } from "oidc-js"; -import type { OidcConfig } from "oidc-js-core"; +import { OidcClient, type OidcClientConfig, type AuthState, type AuthUser, type AuthTokens, type LoginOptions } from "oidc-js"; import type { AuthControllerOptions } from "./types.js"; /** @@ -85,7 +84,7 @@ export class AuthController implements ReactiveController { } /** The OIDC configuration passed to this controller. */ - get config(): OidcConfig { + get config(): OidcClientConfig { return this.options.config; } diff --git a/packages/lit/src/index.ts b/packages/lit/src/index.ts index 2f16b62..932602d 100644 --- a/packages/lit/src/index.ts +++ b/packages/lit/src/index.ts @@ -12,4 +12,5 @@ export type { export type { RequireAuthOptions } from "./require-auth.js"; +export type { OidcClientConfig } from "oidc-js"; export type { OidcConfig, OidcUser, TokenSet } from "oidc-js-core"; diff --git a/packages/lit/src/types.ts b/packages/lit/src/types.ts index 4619d7a..8377cd6 100644 --- a/packages/lit/src/types.ts +++ b/packages/lit/src/types.ts @@ -1,8 +1,6 @@ -import type { OidcConfig } from "oidc-js-core"; - export type { IdTokenClaims, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; -import type { AuthTokens, LoginOptions } from "oidc-js"; +import type { OidcClientConfig, AuthTokens, LoginOptions } from "oidc-js"; import type { OidcUser } from "oidc-js-core"; /** @@ -24,7 +22,7 @@ export interface AuthActions { */ export interface AuthControllerOptions { /** OIDC configuration including issuer, clientId, and redirectUri. */ - config: OidcConfig; + config: OidcClientConfig; /** Whether to fetch the userinfo profile after token exchange. Defaults to true. */ fetchProfile?: boolean; /** diff --git a/packages/preact/package.json b/packages/preact/package.json index 86a748e..1580cba 100644 --- a/packages/preact/package.json +++ b/packages/preact/package.json @@ -1,6 +1,6 @@ { "name": "oidc-js-preact", - "version": "1.1.0", + "version": "1.1.2", "description": "Simple OIDC authentication for Preact. Hooks, provider, and auth guards with zero dependencies.", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/preact/src/context.tsx b/packages/preact/src/context.tsx index f75d63a..929a31c 100644 --- a/packages/preact/src/context.tsx +++ b/packages/preact/src/context.tsx @@ -8,14 +8,13 @@ import { useMemo, } from "preact/hooks"; import type { ComponentChildren } from "preact"; -import { OidcClient, type AuthState, type LoginOptions } from "oidc-js"; -import type { OidcConfig } from "oidc-js-core"; +import { OidcClient, type OidcClientConfig, type AuthState, type LoginOptions } from "oidc-js"; import type { AuthContextValue } from "./types.js"; const AuthContext = createContext(null); interface AuthProviderProps { - config: OidcConfig; + config: OidcClientConfig; fetchProfile?: boolean; onLogin?: (returnTo: string) => void; onError?: (error: Error) => void; diff --git a/packages/preact/src/index.ts b/packages/preact/src/index.ts index 5b67df4..db6fdd9 100644 --- a/packages/preact/src/index.ts +++ b/packages/preact/src/index.ts @@ -10,4 +10,5 @@ export type { LoginOptions, } from "./types.js"; +export type { OidcClientConfig } from "oidc-js"; export type { OidcConfig, OidcUser, TokenSet } from "oidc-js-core"; diff --git a/packages/preact/src/types.ts b/packages/preact/src/types.ts index 5262c37..3b3ddfe 100644 --- a/packages/preact/src/types.ts +++ b/packages/preact/src/types.ts @@ -1,8 +1,6 @@ -import type { OidcConfig } from "oidc-js-core"; - export type { IdTokenClaims, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; -import type { AuthUser, AuthTokens, LoginOptions } from "oidc-js"; +import type { OidcClientConfig, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; import type { OidcUser } from "oidc-js-core"; /** Actions available for controlling the authentication lifecycle. */ @@ -20,7 +18,7 @@ export interface AuthActions { /** The value provided by {@link AuthProvider} and consumed by {@link useAuth}. */ export interface AuthContextValue { /** The OIDC configuration used by the provider. */ - config: OidcConfig; + config: OidcClientConfig; /** The authenticated user, or null if not logged in. */ user: AuthUser | null; /** Whether the user is currently authenticated. */ diff --git a/packages/react/package.json b/packages/react/package.json index df80926..431b3c1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "oidc-js-react", - "version": "1.1.0", + "version": "1.1.2", "description": "Simple OIDC authentication for React. Provider, hooks, and route guards with zero dependencies.", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/react/src/context.tsx b/packages/react/src/context.tsx index 9b88b8b..6cd288a 100644 --- a/packages/react/src/context.tsx +++ b/packages/react/src/context.tsx @@ -8,14 +8,13 @@ import { useMemo, type ReactNode, } from "react"; -import { OidcClient, type AuthState, type LoginOptions } from "oidc-js"; -import type { OidcConfig } from "oidc-js-core"; +import { OidcClient, type OidcClientConfig, type AuthState, type LoginOptions } from "oidc-js"; import type { AuthContextValue } from "./types.js"; const AuthContext = createContext(null); interface AuthProviderProps { - config: OidcConfig; + config: OidcClientConfig; fetchProfile?: boolean; onLogin?: (returnTo: string) => void; onError?: (error: Error) => void; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 5b67df4..db6fdd9 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -10,4 +10,5 @@ export type { LoginOptions, } from "./types.js"; +export type { OidcClientConfig } from "oidc-js"; export type { OidcConfig, OidcUser, TokenSet } from "oidc-js-core"; diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 07a7986..b8eccec 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -1,8 +1,6 @@ -import type { OidcConfig } from "oidc-js-core"; - export type { IdTokenClaims, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; -import type { OidcClient, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; +import type { OidcClient, OidcClientConfig, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; import type { OidcUser } from "oidc-js-core"; export interface AuthActions { @@ -13,7 +11,7 @@ export interface AuthActions { } export interface AuthContextValue { - config: OidcConfig; + config: OidcClientConfig; client: OidcClient; user: AuthUser | null; isAuthenticated: boolean; diff --git a/packages/solid/package.json b/packages/solid/package.json index 5131066..4d1c6e6 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "oidc-js-solid", - "version": "1.1.0", + "version": "1.1.2", "description": "Simple OIDC authentication for SolidJS. Signals, context, and auth guards with zero dependencies.", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/solid/src/context.tsx b/packages/solid/src/context.tsx index 24c13e4..d40e182 100644 --- a/packages/solid/src/context.tsx +++ b/packages/solid/src/context.tsx @@ -6,8 +6,7 @@ import { onCleanup, type ParentComponent, } from "solid-js"; -import { OidcClient, type AuthState, type LoginOptions } from "oidc-js"; -import type { OidcConfig } from "oidc-js-core"; +import { OidcClient, type OidcClientConfig, type AuthState, type LoginOptions } from "oidc-js"; import type { AuthContextValue } from "./types.js"; const AuthContext = createContext(); @@ -17,7 +16,7 @@ const AuthContext = createContext(); */ interface AuthProviderProps { /** OIDC configuration including issuer, clientId, and redirectUri. */ - config: OidcConfig; + config: OidcClientConfig; /** Whether to fetch the userinfo profile after token exchange. Defaults to true. */ fetchProfile?: boolean; /** Callback invoked after a successful login with the returnTo path. */ diff --git a/packages/solid/src/index.ts b/packages/solid/src/index.ts index 5b67df4..db6fdd9 100644 --- a/packages/solid/src/index.ts +++ b/packages/solid/src/index.ts @@ -10,4 +10,5 @@ export type { LoginOptions, } from "./types.js"; +export type { OidcClientConfig } from "oidc-js"; export type { OidcConfig, OidcUser, TokenSet } from "oidc-js-core"; diff --git a/packages/solid/src/types.ts b/packages/solid/src/types.ts index fe4a8a8..0cc883e 100644 --- a/packages/solid/src/types.ts +++ b/packages/solid/src/types.ts @@ -1,8 +1,6 @@ -import type { OidcConfig } from "oidc-js-core"; - export type { IdTokenClaims, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; -import type { AuthUser, AuthTokens, LoginOptions } from "oidc-js"; +import type { OidcClientConfig, AuthUser, AuthTokens, LoginOptions } from "oidc-js"; import type { OidcUser } from "oidc-js-core"; /** @@ -28,7 +26,7 @@ export interface AuthActions { */ export interface AuthContextValue { /** The OIDC configuration used to initialize the provider. */ - config: OidcConfig; + config: OidcClientConfig; /** The authenticated user, or null if not logged in. */ user: AuthUser | null; /** Whether the user is currently authenticated. */ diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6597adb..518f887 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "oidc-js-svelte", - "version": "1.1.0", + "version": "1.1.2", "description": "Simple OIDC authentication for Svelte 5. Context, runes, and auth guards with zero dependencies.", "type": "module", "svelte": "./dist/index.js", diff --git a/packages/svelte/src/AuthProvider.svelte b/packages/svelte/src/AuthProvider.svelte index 1d0cf58..116d54c 100644 --- a/packages/svelte/src/AuthProvider.svelte +++ b/packages/svelte/src/AuthProvider.svelte @@ -16,14 +16,14 @@ ``` --> - + {#snippet children()} {#if path === "/callback"} diff --git a/tests/e2e/vue-app/src/main.ts b/tests/e2e/vue-app/src/main.ts index 51a617a..59ea3d2 100644 --- a/tests/e2e/vue-app/src/main.ts +++ b/tests/e2e/vue-app/src/main.ts @@ -30,8 +30,8 @@ app.use(oidcPlugin, { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, + fetchProfile, }, - fetchProfile, onLogin(returnTo: string) { router.replace(returnTo); }, From 246923ab523c14730727a72a7db5f0fa20e71b61 Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 16:33:15 -0700 Subject: [PATCH 09/14] test: add e2e test for proactive auto-refresh and rename spec Add autoRefreshInterval localStorage toggle to all 8 e2e apps and a new Proactive Auto-Refresh test that sets a 1s polling interval, simulates token expiry, and verifies the interval fires a refresh without user interaction. Rename login.spec.ts to oidc.spec.ts to reflect the broader scope of the test suite. Co-Authored-By: Claude Opus 4.6 --- tests/e2e/angular-app/src/main.ts | 2 ++ .../e2e/kasper-app/src/components/App.kasper | 2 ++ tests/e2e/lit-app/src/app-root.ts | 2 ++ tests/e2e/preact-app/src/index.tsx | 2 ++ tests/e2e/react-app/src/main.tsx | 2 ++ tests/e2e/solid-app/src/App.tsx | 2 ++ .../e2e/specs/{login.spec.ts => oidc.spec.ts} | 33 +++++++++++++++++++ tests/e2e/svelte-app/src/App.svelte | 2 ++ tests/e2e/vue-app/src/main.ts | 2 ++ 9 files changed, 49 insertions(+) rename tests/e2e/specs/{login.spec.ts => oidc.spec.ts} (94%) diff --git a/tests/e2e/angular-app/src/main.ts b/tests/e2e/angular-app/src/main.ts index 963678b..c2c889a 100644 --- a/tests/e2e/angular-app/src/main.ts +++ b/tests/e2e/angular-app/src/main.ts @@ -7,6 +7,7 @@ import { AppComponent } from "./app/app.component"; import { routes } from "./app/app.routes"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; +const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); // eslint-disable-next-line @typescript-eslint/no-explicit-any const viteEnv = (import.meta as any).env ?? {}; const idpPort = viteEnv.VITE_IDP_PORT ?? "9999"; @@ -18,6 +19,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, + ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), }; bootstrapApplication(AppComponent, { diff --git a/tests/e2e/kasper-app/src/components/App.kasper b/tests/e2e/kasper-app/src/components/App.kasper index 2de757f..f407ff1 100644 --- a/tests/e2e/kasper-app/src/components/App.kasper +++ b/tests/e2e/kasper-app/src/components/App.kasper @@ -20,6 +20,7 @@ export class AppRoot extends Component { fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; config = (() => { + const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; return { @@ -28,6 +29,7 @@ export class AppRoot extends Component { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, + ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), }; })(); diff --git a/tests/e2e/lit-app/src/app-root.ts b/tests/e2e/lit-app/src/app-root.ts index b488f99..a0ecafa 100644 --- a/tests/e2e/lit-app/src/app-root.ts +++ b/tests/e2e/lit-app/src/app-root.ts @@ -7,6 +7,7 @@ import "./pages/protected-a-page.js"; import "./pages/protected-b-page.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; +const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -16,6 +17,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, + ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), }; @customElement("app-root") diff --git a/tests/e2e/preact-app/src/index.tsx b/tests/e2e/preact-app/src/index.tsx index 6d47003..c0ef4df 100644 --- a/tests/e2e/preact-app/src/index.tsx +++ b/tests/e2e/preact-app/src/index.tsx @@ -5,6 +5,7 @@ import { useCallback } from "preact/hooks"; import { App } from "./App.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; +const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -14,6 +15,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, + ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), }; function Root() { diff --git a/tests/e2e/react-app/src/main.tsx b/tests/e2e/react-app/src/main.tsx index e66706f..a593df9 100644 --- a/tests/e2e/react-app/src/main.tsx +++ b/tests/e2e/react-app/src/main.tsx @@ -5,6 +5,7 @@ import { useCallback } from "react"; import { App } from "./App.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; +const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -14,6 +15,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, + ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), }; function Root() { diff --git a/tests/e2e/solid-app/src/App.tsx b/tests/e2e/solid-app/src/App.tsx index 1893bdf..5b5152d 100644 --- a/tests/e2e/solid-app/src/App.tsx +++ b/tests/e2e/solid-app/src/App.tsx @@ -3,6 +3,7 @@ import { AuthProvider } from "oidc-js-solid"; import type { ParentComponent } from "solid-js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; +const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -12,6 +13,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, + ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), }; export const App: ParentComponent = (props) => { diff --git a/tests/e2e/specs/login.spec.ts b/tests/e2e/specs/oidc.spec.ts similarity index 94% rename from tests/e2e/specs/login.spec.ts rename to tests/e2e/specs/oidc.spec.ts index e42523a..fec3de0 100644 --- a/tests/e2e/specs/login.spec.ts +++ b/tests/e2e/specs/oidc.spec.ts @@ -576,6 +576,39 @@ test.describe(`[${FRAMEWORK}] RequireAuth`, () => { }); }); +test.describe(`[${FRAMEWORK}] Proactive Auto-Refresh`, () => { + // Configures autoRefreshInterval=1s via localStorage, logs in, simulates token expiry, + // and verifies that the polling interval automatically triggers a token refresh + // without any user interaction (no navigation, no button click). + test("auto-refresh polling refreshes expired token without user interaction", async ({ page }) => { + const traffic = trackTraffic(page); + // Set 1-second polling interval before the app loads with it + await page.goto("/", { waitUntil: "networkidle" }); + await page.evaluate(() => localStorage.setItem("e2e-autoRefreshInterval", "1")); + + // login() calls page.goto("/") which reloads with the new interval + await login(page); + const oldToken = await page.getByTestId("access-token-value").textContent(); + const expiresAt = Number(await page.getByTestId("expires-at").textContent()); + + // Snapshot request count after login, before auto-refresh fires + const requestCountAfterLogin = traffic.requests().length; + + // Advance Date.now past token expiry — the 1s interval will detect it and auto-refresh + await simulateTokenExpiry(page, expiresAt); + + // Token should change without any user interaction (no click, no navigation) + await expect(page.getByTestId("access-token-value")).not.toHaveText(oldToken!, { timeout: TIMEOUT }); + await expect(page.getByTestId("authenticated")).toBeVisible(); + + // Verify that auto-refresh issued a POST /token after login completed + const autoRefreshRequests = traffic.requests().slice(requestCountAfterLogin); + expect(autoRefreshRequests).toContain(POST_TOKEN); + + await page.evaluate(() => localStorage.removeItem("e2e-autoRefreshInterval")); + }); +}); + test.describe(`[${FRAMEWORK}] Nonce Validation`, () => { // Tampers with the stored nonce before callback. The library should detect the mismatch // and surface an error instead of accepting the tokens (prevents token injection/replay). diff --git a/tests/e2e/svelte-app/src/App.svelte b/tests/e2e/svelte-app/src/App.svelte index 2248005..a7ed752 100644 --- a/tests/e2e/svelte-app/src/App.svelte +++ b/tests/e2e/svelte-app/src/App.svelte @@ -6,6 +6,7 @@ import ProtectedB from "./routes/ProtectedB.svelte"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; + const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -15,6 +16,7 @@ redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, + ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), }; let path = $state(window.location.pathname); diff --git a/tests/e2e/vue-app/src/main.ts b/tests/e2e/vue-app/src/main.ts index 59ea3d2..b46548f 100644 --- a/tests/e2e/vue-app/src/main.ts +++ b/tests/e2e/vue-app/src/main.ts @@ -8,6 +8,7 @@ import ProtectedA from "./views/ProtectedA.vue"; import ProtectedB from "./views/ProtectedB.vue"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; +const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -31,6 +32,7 @@ app.use(oidcPlugin, { scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, fetchProfile, + ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), }, onLogin(returnTo: string) { router.replace(returnTo); From fbf02d0fc34e869e93dc7d5039592a995a7a4756 Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 16:34:45 -0700 Subject: [PATCH 10/14] style: group localStorage toggles in e2e apps Co-Authored-By: Claude Opus 4.6 --- tests/e2e/angular-app/src/main.ts | 1 + tests/e2e/lit-app/src/app-root.ts | 1 + tests/e2e/preact-app/src/index.tsx | 1 + tests/e2e/react-app/src/main.tsx | 1 + tests/e2e/solid-app/src/App.tsx | 1 + tests/e2e/svelte-app/src/App.svelte | 1 + tests/e2e/vue-app/src/main.ts | 1 + 7 files changed, 7 insertions(+) diff --git a/tests/e2e/angular-app/src/main.ts b/tests/e2e/angular-app/src/main.ts index c2c889a..abf477d 100644 --- a/tests/e2e/angular-app/src/main.ts +++ b/tests/e2e/angular-app/src/main.ts @@ -8,6 +8,7 @@ import { routes } from "./app/app.routes"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const viteEnv = (import.meta as any).env ?? {}; const idpPort = viteEnv.VITE_IDP_PORT ?? "9999"; diff --git a/tests/e2e/lit-app/src/app-root.ts b/tests/e2e/lit-app/src/app-root.ts index a0ecafa..691d4dc 100644 --- a/tests/e2e/lit-app/src/app-root.ts +++ b/tests/e2e/lit-app/src/app-root.ts @@ -8,6 +8,7 @@ import "./pages/protected-b-page.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; diff --git a/tests/e2e/preact-app/src/index.tsx b/tests/e2e/preact-app/src/index.tsx index c0ef4df..89c5e69 100644 --- a/tests/e2e/preact-app/src/index.tsx +++ b/tests/e2e/preact-app/src/index.tsx @@ -6,6 +6,7 @@ import { App } from "./App.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; diff --git a/tests/e2e/react-app/src/main.tsx b/tests/e2e/react-app/src/main.tsx index a593df9..3b0bae3 100644 --- a/tests/e2e/react-app/src/main.tsx +++ b/tests/e2e/react-app/src/main.tsx @@ -6,6 +6,7 @@ import { App } from "./App.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; diff --git a/tests/e2e/solid-app/src/App.tsx b/tests/e2e/solid-app/src/App.tsx index 5b5152d..a594042 100644 --- a/tests/e2e/solid-app/src/App.tsx +++ b/tests/e2e/solid-app/src/App.tsx @@ -4,6 +4,7 @@ import type { ParentComponent } from "solid-js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; diff --git a/tests/e2e/svelte-app/src/App.svelte b/tests/e2e/svelte-app/src/App.svelte index a7ed752..18318b3 100644 --- a/tests/e2e/svelte-app/src/App.svelte +++ b/tests/e2e/svelte-app/src/App.svelte @@ -7,6 +7,7 @@ const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; diff --git a/tests/e2e/vue-app/src/main.ts b/tests/e2e/vue-app/src/main.ts index b46548f..b36b8bf 100644 --- a/tests/e2e/vue-app/src/main.ts +++ b/tests/e2e/vue-app/src/main.ts @@ -9,6 +9,7 @@ import ProtectedB from "./views/ProtectedB.vue"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; From cfa2a729ec42195417f0909bdf6da33a54f269e0 Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 16:36:08 -0700 Subject: [PATCH 11/14] fix: parse autoRefreshInterval as number at read site in e2e apps Co-Authored-By: Claude Opus 4.6 --- tests/e2e/angular-app/src/main.ts | 4 ++-- tests/e2e/kasper-app/src/components/App.kasper | 4 ++-- tests/e2e/lit-app/src/app-root.ts | 4 ++-- tests/e2e/preact-app/src/index.tsx | 4 ++-- tests/e2e/react-app/src/main.tsx | 4 ++-- tests/e2e/solid-app/src/App.tsx | 4 ++-- tests/e2e/svelte-app/src/App.svelte | 4 ++-- tests/e2e/vue-app/src/main.ts | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/e2e/angular-app/src/main.ts b/tests/e2e/angular-app/src/main.ts index abf477d..4dd7f6d 100644 --- a/tests/e2e/angular-app/src/main.ts +++ b/tests/e2e/angular-app/src/main.ts @@ -7,7 +7,7 @@ import { AppComponent } from "./app/app.component"; import { routes } from "./app/app.routes"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; -const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); +const autoRefreshInterval = Number(localStorage.getItem("e2e-autoRefreshInterval")); // eslint-disable-next-line @typescript-eslint/no-explicit-any const viteEnv = (import.meta as any).env ?? {}; @@ -20,7 +20,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), + ...(autoRefreshInterval ? { autoRefreshInterval } : {}), }; bootstrapApplication(AppComponent, { diff --git a/tests/e2e/kasper-app/src/components/App.kasper b/tests/e2e/kasper-app/src/components/App.kasper index f407ff1..5697f4d 100644 --- a/tests/e2e/kasper-app/src/components/App.kasper +++ b/tests/e2e/kasper-app/src/components/App.kasper @@ -20,7 +20,7 @@ export class AppRoot extends Component { fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; config = (() => { - const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + const autoRefreshInterval = Number(localStorage.getItem("e2e-autoRefreshInterval")); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; return { @@ -29,7 +29,7 @@ export class AppRoot extends Component { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), + ...(autoRefreshInterval ? { autoRefreshInterval } : {}), }; })(); diff --git a/tests/e2e/lit-app/src/app-root.ts b/tests/e2e/lit-app/src/app-root.ts index 691d4dc..c01a077 100644 --- a/tests/e2e/lit-app/src/app-root.ts +++ b/tests/e2e/lit-app/src/app-root.ts @@ -7,7 +7,7 @@ import "./pages/protected-a-page.js"; import "./pages/protected-b-page.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; -const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); +const autoRefreshInterval = Number(localStorage.getItem("e2e-autoRefreshInterval")); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -18,7 +18,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), + ...(autoRefreshInterval ? { autoRefreshInterval } : {}), }; @customElement("app-root") diff --git a/tests/e2e/preact-app/src/index.tsx b/tests/e2e/preact-app/src/index.tsx index 89c5e69..3120781 100644 --- a/tests/e2e/preact-app/src/index.tsx +++ b/tests/e2e/preact-app/src/index.tsx @@ -5,7 +5,7 @@ import { useCallback } from "preact/hooks"; import { App } from "./App.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; -const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); +const autoRefreshInterval = Number(localStorage.getItem("e2e-autoRefreshInterval")); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -16,7 +16,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), + ...(autoRefreshInterval ? { autoRefreshInterval } : {}), }; function Root() { diff --git a/tests/e2e/react-app/src/main.tsx b/tests/e2e/react-app/src/main.tsx index 3b0bae3..2d7db93 100644 --- a/tests/e2e/react-app/src/main.tsx +++ b/tests/e2e/react-app/src/main.tsx @@ -5,7 +5,7 @@ import { useCallback } from "react"; import { App } from "./App.js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; -const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); +const autoRefreshInterval = Number(localStorage.getItem("e2e-autoRefreshInterval")); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -16,7 +16,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), + ...(autoRefreshInterval ? { autoRefreshInterval } : {}), }; function Root() { diff --git a/tests/e2e/solid-app/src/App.tsx b/tests/e2e/solid-app/src/App.tsx index a594042..9f86408 100644 --- a/tests/e2e/solid-app/src/App.tsx +++ b/tests/e2e/solid-app/src/App.tsx @@ -3,7 +3,7 @@ import { AuthProvider } from "oidc-js-solid"; import type { ParentComponent } from "solid-js"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; -const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); +const autoRefreshInterval = Number(localStorage.getItem("e2e-autoRefreshInterval")); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -14,7 +14,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), + ...(autoRefreshInterval ? { autoRefreshInterval } : {}), }; export const App: ParentComponent = (props) => { diff --git a/tests/e2e/svelte-app/src/App.svelte b/tests/e2e/svelte-app/src/App.svelte index 18318b3..ad0da7d 100644 --- a/tests/e2e/svelte-app/src/App.svelte +++ b/tests/e2e/svelte-app/src/App.svelte @@ -6,7 +6,7 @@ import ProtectedB from "./routes/ProtectedB.svelte"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; - const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); + const autoRefreshInterval = Number(localStorage.getItem("e2e-autoRefreshInterval")); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -17,7 +17,7 @@ redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), + ...(autoRefreshInterval ? { autoRefreshInterval } : {}), }; let path = $state(window.location.pathname); diff --git a/tests/e2e/vue-app/src/main.ts b/tests/e2e/vue-app/src/main.ts index b36b8bf..1b357a2 100644 --- a/tests/e2e/vue-app/src/main.ts +++ b/tests/e2e/vue-app/src/main.ts @@ -8,7 +8,7 @@ import ProtectedA from "./views/ProtectedA.vue"; import ProtectedB from "./views/ProtectedB.vue"; const fetchProfile = localStorage.getItem("e2e-fetchProfile") !== "false"; -const autoRefreshInterval = localStorage.getItem("e2e-autoRefreshInterval"); +const autoRefreshInterval = Number(localStorage.getItem("e2e-autoRefreshInterval")); const idpPort = import.meta.env.VITE_IDP_PORT ?? "9999"; const appPort = import.meta.env.VITE_APP_PORT ?? "5173"; @@ -33,7 +33,7 @@ app.use(oidcPlugin, { scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, fetchProfile, - ...(autoRefreshInterval ? { autoRefreshInterval: Number(autoRefreshInterval) } : {}), + ...(autoRefreshInterval ? { autoRefreshInterval } : {}), }, onLogin(returnTo: string) { router.replace(returnTo); From 41f83cd5f77e4f87b292b36ef75ce58fbb696ebe Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 16:37:06 -0700 Subject: [PATCH 12/14] refactor: simplify autoRefreshInterval config in e2e apps Co-Authored-By: Claude Opus 4.6 --- tests/e2e/angular-app/src/main.ts | 2 +- tests/e2e/kasper-app/src/components/App.kasper | 2 +- tests/e2e/lit-app/src/app-root.ts | 2 +- tests/e2e/preact-app/src/index.tsx | 2 +- tests/e2e/react-app/src/main.tsx | 2 +- tests/e2e/solid-app/src/App.tsx | 2 +- tests/e2e/svelte-app/src/App.svelte | 2 +- tests/e2e/vue-app/src/main.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/e2e/angular-app/src/main.ts b/tests/e2e/angular-app/src/main.ts index 4dd7f6d..44b0ac9 100644 --- a/tests/e2e/angular-app/src/main.ts +++ b/tests/e2e/angular-app/src/main.ts @@ -20,7 +20,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval } : {}), + autoRefreshInterval: autoRefreshInterval || undefined, }; bootstrapApplication(AppComponent, { diff --git a/tests/e2e/kasper-app/src/components/App.kasper b/tests/e2e/kasper-app/src/components/App.kasper index 5697f4d..bfb8cf0 100644 --- a/tests/e2e/kasper-app/src/components/App.kasper +++ b/tests/e2e/kasper-app/src/components/App.kasper @@ -29,7 +29,7 @@ export class AppRoot extends Component { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval } : {}), + autoRefreshInterval: autoRefreshInterval || undefined, }; })(); diff --git a/tests/e2e/lit-app/src/app-root.ts b/tests/e2e/lit-app/src/app-root.ts index c01a077..14c4dea 100644 --- a/tests/e2e/lit-app/src/app-root.ts +++ b/tests/e2e/lit-app/src/app-root.ts @@ -18,7 +18,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval } : {}), + autoRefreshInterval: autoRefreshInterval || undefined, }; @customElement("app-root") diff --git a/tests/e2e/preact-app/src/index.tsx b/tests/e2e/preact-app/src/index.tsx index 3120781..8370ba2 100644 --- a/tests/e2e/preact-app/src/index.tsx +++ b/tests/e2e/preact-app/src/index.tsx @@ -16,7 +16,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval } : {}), + autoRefreshInterval: autoRefreshInterval || undefined, }; function Root() { diff --git a/tests/e2e/react-app/src/main.tsx b/tests/e2e/react-app/src/main.tsx index 2d7db93..55fc10a 100644 --- a/tests/e2e/react-app/src/main.tsx +++ b/tests/e2e/react-app/src/main.tsx @@ -16,7 +16,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval } : {}), + autoRefreshInterval: autoRefreshInterval || undefined, }; function Root() { diff --git a/tests/e2e/solid-app/src/App.tsx b/tests/e2e/solid-app/src/App.tsx index 9f86408..5b109ba 100644 --- a/tests/e2e/solid-app/src/App.tsx +++ b/tests/e2e/solid-app/src/App.tsx @@ -14,7 +14,7 @@ const config = { redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval } : {}), + autoRefreshInterval: autoRefreshInterval || undefined, }; export const App: ParentComponent = (props) => { diff --git a/tests/e2e/svelte-app/src/App.svelte b/tests/e2e/svelte-app/src/App.svelte index ad0da7d..aa57042 100644 --- a/tests/e2e/svelte-app/src/App.svelte +++ b/tests/e2e/svelte-app/src/App.svelte @@ -17,7 +17,7 @@ redirectUri: `http://localhost:${appPort}/callback`, scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, - ...(autoRefreshInterval ? { autoRefreshInterval } : {}), + autoRefreshInterval: autoRefreshInterval || undefined, }; let path = $state(window.location.pathname); diff --git a/tests/e2e/vue-app/src/main.ts b/tests/e2e/vue-app/src/main.ts index 1b357a2..9b566fc 100644 --- a/tests/e2e/vue-app/src/main.ts +++ b/tests/e2e/vue-app/src/main.ts @@ -33,7 +33,7 @@ app.use(oidcPlugin, { scopes: ["openid", "profile", "email", "offline_access"], postLogoutRedirectUri: `http://localhost:${appPort}`, fetchProfile, - ...(autoRefreshInterval ? { autoRefreshInterval } : {}), + autoRefreshInterval: autoRefreshInterval || undefined, }, onLogin(returnTo: string) { router.replace(returnTo); From 3ca571a867c008010c68af851e5a3e30080290b8 Mon Sep 17 00:00:00 2001 From: eugenioenko Date: Fri, 8 May 2026 16:45:15 -0700 Subject: [PATCH 13/14] fix: move fetchProfile into config in kasper e2e app The kasper app was still passing fetchProfile as a separate template attribute, but the AuthProvider no longer reads it as a separate arg. Co-Authored-By: Claude Opus 4.6 --- tests/e2e/kasper-app/src/components/App.kasper | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/e2e/kasper-app/src/components/App.kasper b/tests/e2e/kasper-app/src/components/App.kasper index bfb8cf0..5795c2f 100644 --- a/tests/e2e/kasper-app/src/components/App.kasper +++ b/tests/e2e/kasper-app/src/components/App.kasper @@ -1,5 +1,5 @@