@douvery/auth 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +176 -1
- package/dist/index.js +137 -2
- package/dist/index.js.map +1 -1
- package/dist/qwik/index.d.ts +100 -27
- package/dist/qwik/index.js +183 -59
- package/dist/qwik/index.js.map +1 -1
- package/dist/session/index.d.ts +205 -0
- package/dist/session/index.js +311 -0
- package/dist/session/index.js.map +1 -0
- package/package.json +17 -11
- package/src/qwik/index.tsx +263 -75
- package/src/qwik/session.ts +72 -0
package/dist/index.d.ts
CHANGED
|
@@ -454,5 +454,180 @@ declare class TokenManager {
|
|
|
454
454
|
clearReturnTo(): Promise<void>;
|
|
455
455
|
clearAll(): Promise<void>;
|
|
456
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Options for createServerBridgedStorage.
|
|
459
|
+
*
|
|
460
|
+
* Use this when tokens are managed server-side (httpOnly cookies)
|
|
461
|
+
* but the OAuth/PKCE flow needs client-side ephemeral storage.
|
|
462
|
+
*/
|
|
463
|
+
interface ServerBridgedStorageOptions {
|
|
464
|
+
/**
|
|
465
|
+
* Name of a **non-httpOnly** cookie that holds the access token
|
|
466
|
+
* expiration timestamp (in milliseconds). Used to infer whether
|
|
467
|
+
* a valid session exists without exposing the actual tokens.
|
|
468
|
+
*/
|
|
469
|
+
tokenExpirationCookie: string;
|
|
470
|
+
/**
|
|
471
|
+
* Placeholder value returned by `get()` for server-managed keys
|
|
472
|
+
* (accessToken, refreshToken). Signals to the caller that the
|
|
473
|
+
* real token exists but is not readable from JS.
|
|
474
|
+
* @default "__server_managed__"
|
|
475
|
+
*/
|
|
476
|
+
serverManagedPlaceholder?: string;
|
|
477
|
+
/**
|
|
478
|
+
* Enable debug logging.
|
|
479
|
+
* @default false
|
|
480
|
+
*/
|
|
481
|
+
debug?: boolean;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Creates a `TokenStorage` adapter for apps where **tokens are
|
|
485
|
+
* managed server-side** (e.g. httpOnly cookies set by routeLoader$/
|
|
486
|
+
* routeAction$) but the OAuth PKCE flow still needs ephemeral
|
|
487
|
+
* client-side storage for state, nonce, codeVerifier and returnTo.
|
|
488
|
+
*
|
|
489
|
+
* Behaviour per key category:
|
|
490
|
+
*
|
|
491
|
+
* | Category | get() | set() / remove() |
|
|
492
|
+
* |-------------------|------------------------------------------|-------------------|
|
|
493
|
+
* | accessToken | returns placeholder if session is active | no-op (server) |
|
|
494
|
+
* | refreshToken | returns placeholder if session exists | no-op (server) |
|
|
495
|
+
* | idToken | always null | no-op |
|
|
496
|
+
* | expiresAt | reads from expiration cookie | no-op (server) |
|
|
497
|
+
* | state/nonce/etc. | sessionStorage | sessionStorage |
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```ts
|
|
501
|
+
* import { createServerBridgedStorage } from '@douvery/auth';
|
|
502
|
+
*
|
|
503
|
+
* const bridgedStorage = createServerBridgedStorage({
|
|
504
|
+
* tokenExpirationCookie: 'dou_token_exp',
|
|
505
|
+
* debug: import.meta.env.DEV,
|
|
506
|
+
* });
|
|
507
|
+
*
|
|
508
|
+
* const config: DouveryAuthConfig = {
|
|
509
|
+
* clientId: 'my-app',
|
|
510
|
+
* redirectUri: '/callback',
|
|
511
|
+
* customStorage: bridgedStorage,
|
|
512
|
+
* autoRefresh: false, // server handles refresh
|
|
513
|
+
* };
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
declare function createServerBridgedStorage(options: ServerBridgedStorageOptions): TokenStorage;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @douvery/auth - Session Types
|
|
520
|
+
* Framework-agnostic opaque session resolution types
|
|
521
|
+
*/
|
|
522
|
+
/** Configuration for the session resolver factory */
|
|
523
|
+
interface SessionResolverConfig {
|
|
524
|
+
/** Auth server session API base URL (e.g. "http://localhost:9924/api/session") */
|
|
525
|
+
sessionApiUrl: string;
|
|
526
|
+
/**
|
|
527
|
+
* Internal service name for IdP UI auth via X-Douvery-Internal-Service header.
|
|
528
|
+
* Used by the IdP's own UI (like accounts.google.com) — NOT an OAuth client.
|
|
529
|
+
* Requires internalServiceSecret.
|
|
530
|
+
*/
|
|
531
|
+
internalServiceName?: string;
|
|
532
|
+
/**
|
|
533
|
+
* HMAC secret for internal service auth.
|
|
534
|
+
* Signs X-Douvery-Internal-Signature: HMAC-SHA256(timestamp:serviceName, secret).
|
|
535
|
+
* Used together with internalServiceName.
|
|
536
|
+
*/
|
|
537
|
+
internalServiceSecret?: string;
|
|
538
|
+
/**
|
|
539
|
+
* OAuth Client ID for consumer app auth via X-Client-Id header.
|
|
540
|
+
* For apps that ARE OAuth clients (douvery-web, center, mobile).
|
|
541
|
+
*/
|
|
542
|
+
clientId?: string;
|
|
543
|
+
/**
|
|
544
|
+
* OAuth Client Secret for consumer app auth via X-Client-Secret header.
|
|
545
|
+
* Used together with clientId. Validated against oauth_clients table (Argon2).
|
|
546
|
+
*/
|
|
547
|
+
clientSecret?: string;
|
|
548
|
+
/** Cookie name for the session ID (e.g. "douvery-session") */
|
|
549
|
+
cookieName: string;
|
|
550
|
+
/** Session cookie max age in seconds @default 2592000 (30 days) */
|
|
551
|
+
cookieMaxAge?: number;
|
|
552
|
+
/** Whether cookies should use the 'secure' flag @default true */
|
|
553
|
+
secureCookies?: boolean;
|
|
554
|
+
/** Timeout for lightweight session API calls like /token (ms) @default 3000 */
|
|
555
|
+
fetchTimeoutMs?: number;
|
|
556
|
+
/** Timeout for heavy session API calls like /refresh (ms) @default 8000 */
|
|
557
|
+
refreshTimeoutMs?: number;
|
|
558
|
+
/** Fallback cache TTL when JWT exp cannot be parsed (ms) @default 30000 */
|
|
559
|
+
fallbackCacheTtlMs?: number;
|
|
560
|
+
/** Enable debug logging @default false */
|
|
561
|
+
debug?: boolean;
|
|
562
|
+
/** Custom logger implementation. Defaults to console when debug is true. */
|
|
563
|
+
logger?: SessionLogger;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Minimal cookie adapter interface.
|
|
567
|
+
* Each framework implements this for its own cookie API (Qwik, Next.js, Express, etc).
|
|
568
|
+
*
|
|
569
|
+
* The resolver uses the adapter object reference as a WeakMap key for per-request
|
|
570
|
+
* caching. Ensure the SAME adapter instance is used for all calls within a single
|
|
571
|
+
* request to benefit from deduplication and caching.
|
|
572
|
+
*/
|
|
573
|
+
interface CookieAdapter {
|
|
574
|
+
/** Read a cookie value by name. Returns undefined if not found. */
|
|
575
|
+
get(name: string): string | undefined;
|
|
576
|
+
/** Set a cookie with the given name, value, and options. */
|
|
577
|
+
set(name: string, value: string, options: CookieSetOptions): void;
|
|
578
|
+
}
|
|
579
|
+
/** Options for setting a cookie (standard HTTP cookie attributes) */
|
|
580
|
+
interface CookieSetOptions {
|
|
581
|
+
path?: string;
|
|
582
|
+
httpOnly?: boolean;
|
|
583
|
+
secure?: boolean;
|
|
584
|
+
sameSite?: "strict" | "lax" | "none";
|
|
585
|
+
maxAge?: number;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Result of a refresh attempt. Callers MUST distinguish between:
|
|
589
|
+
* - 'success': tokens refreshed, cache invalidated — proceed normally
|
|
590
|
+
* - 'definitive_failure': server confirmed session is dead (401/404) — safe to clear cookie
|
|
591
|
+
* - 'transient_failure': timeout/network/500 — session may still be valid, DON'T clear cookie
|
|
592
|
+
*/
|
|
593
|
+
type RefreshResult = "success" | "definitive_failure" | "transient_failure";
|
|
594
|
+
interface SessionLogger {
|
|
595
|
+
debug(...args: unknown[]): void;
|
|
596
|
+
warn(...args: unknown[]): void;
|
|
597
|
+
error(...args: unknown[]): void;
|
|
598
|
+
}
|
|
599
|
+
/** The public API returned by createSessionResolver() */
|
|
600
|
+
interface SessionResolver {
|
|
601
|
+
/**
|
|
602
|
+
* Resolve opaque session to JWT access_token (async).
|
|
603
|
+
* Uses per-request caching and deduplication.
|
|
604
|
+
* Returns the JWT even if expired — the caller handles refresh.
|
|
605
|
+
*/
|
|
606
|
+
getAccessToken(cookies: CookieAdapter): Promise<string | undefined>;
|
|
607
|
+
/**
|
|
608
|
+
* Synchronous access to cached token (for sync header builders).
|
|
609
|
+
* Returns cached value only — NO network call.
|
|
610
|
+
*/
|
|
611
|
+
getAccessTokenSync(cookies: CookieAdapter): string | undefined;
|
|
612
|
+
/**
|
|
613
|
+
* Refresh session tokens via auth server.
|
|
614
|
+
* Triggers full token rotation. Deduplicates concurrent refresh calls
|
|
615
|
+
* across all requests for the same session_id.
|
|
616
|
+
*/
|
|
617
|
+
refreshSession(cookies: CookieAdapter): Promise<RefreshResult>;
|
|
618
|
+
/** Destroy session on auth server and clear local cookie. */
|
|
619
|
+
destroySession(cookies: CookieAdapter): Promise<void>;
|
|
620
|
+
/** Save session ID in an HttpOnly cookie after OAuth callback. */
|
|
621
|
+
setSessionCookie(sessionId: string, cookies: CookieAdapter): void;
|
|
622
|
+
/** Read session ID from cookie. */
|
|
623
|
+
getSessionId(cookies: CookieAdapter): string | undefined;
|
|
624
|
+
/** Check if user has an active session cookie. */
|
|
625
|
+
hasSession(cookies: CookieAdapter): boolean;
|
|
626
|
+
/**
|
|
627
|
+
* Clear session cookie and invalidate cached token.
|
|
628
|
+
* Sets an expired cookie to ensure the browser removes it.
|
|
629
|
+
*/
|
|
630
|
+
clearSessionCookie(cookies: CookieAdapter): void;
|
|
631
|
+
}
|
|
457
632
|
|
|
458
|
-
export { type AddAccountOptions, AuthError, type AuthErrorCode, type AuthEvent, type AuthEventHandler, type AuthNavigationOptions, type AuthState, type AuthStatus, type AuthUrl, type CallbackResult, CookieStorage, type DecodedIdToken, DouveryAuthClient, type DouveryAuthConfig, LocalStorage, type LoginOptions, type LogoutOptions, MemoryStorage, type OIDCDiscovery, type PKCEPair, type RecoverAccountOptions, type RegisterOptions, type RevokeTokenOptions, STORAGE_KEYS, type SelectAccountOptions, SessionStorage, type SetupAddressOptions, type SetupPasskeyOptions, type StorageKeys, type TokenInfo, TokenManager, type TokenSet, type TokenStorage, type UpgradeAccountOptions, type User, type VerifyAccountOptions, base64UrlDecode, base64UrlEncode, createDouveryAuth, createStorage, decodeJWT, generateCodeChallenge, generateCodeVerifier, generateNonce, generatePKCEPair, generateState, getTokenExpiration, isTokenExpired, verifyCodeChallenge };
|
|
633
|
+
export { type AddAccountOptions, AuthError, type AuthErrorCode, type AuthEvent, type AuthEventHandler, type AuthNavigationOptions, type AuthState, type AuthStatus, type AuthUrl, type CallbackResult, type CookieAdapter, type CookieSetOptions, CookieStorage, type DecodedIdToken, DouveryAuthClient, type DouveryAuthConfig, LocalStorage, type LoginOptions, type LogoutOptions, MemoryStorage, type OIDCDiscovery, type PKCEPair, type RecoverAccountOptions, type RefreshResult, type RegisterOptions, type RevokeTokenOptions, STORAGE_KEYS, type SelectAccountOptions, type ServerBridgedStorageOptions, type SessionLogger, type SessionResolver, type SessionResolverConfig, SessionStorage, type SetupAddressOptions, type SetupPasskeyOptions, type StorageKeys, type TokenInfo, TokenManager, type TokenSet, type TokenStorage, type UpgradeAccountOptions, type User, type VerifyAccountOptions, base64UrlDecode, base64UrlEncode, createDouveryAuth, createServerBridgedStorage, createStorage, decodeJWT, generateCodeChallenge, generateCodeVerifier, generateNonce, generatePKCEPair, generateState, getTokenExpiration, isTokenExpired, verifyCodeChallenge };
|
package/dist/index.js
CHANGED
|
@@ -286,6 +286,113 @@ var TokenManager = class {
|
|
|
286
286
|
await this.storage.clear();
|
|
287
287
|
}
|
|
288
288
|
};
|
|
289
|
+
var SERVER_TOKEN_KEYS = /* @__PURE__ */ new Set([
|
|
290
|
+
STORAGE_KEYS.accessToken,
|
|
291
|
+
STORAGE_KEYS.refreshToken,
|
|
292
|
+
STORAGE_KEYS.idToken
|
|
293
|
+
]);
|
|
294
|
+
var PKCE_KEYS = /* @__PURE__ */ new Set([
|
|
295
|
+
STORAGE_KEYS.state,
|
|
296
|
+
STORAGE_KEYS.nonce,
|
|
297
|
+
STORAGE_KEYS.codeVerifier,
|
|
298
|
+
STORAGE_KEYS.returnTo
|
|
299
|
+
]);
|
|
300
|
+
function readClientCookie(name) {
|
|
301
|
+
if (typeof document === "undefined") return null;
|
|
302
|
+
const cookies = document.cookie.split(";");
|
|
303
|
+
for (const c of cookies) {
|
|
304
|
+
const [key, ...parts] = c.trim().split("=");
|
|
305
|
+
if (key === name) return decodeURIComponent(parts.join("="));
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
function safeSessionStorage() {
|
|
310
|
+
if (typeof window === "undefined" || typeof sessionStorage === "undefined") {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
return sessionStorage;
|
|
314
|
+
}
|
|
315
|
+
function createServerBridgedStorage(options) {
|
|
316
|
+
const {
|
|
317
|
+
tokenExpirationCookie,
|
|
318
|
+
serverManagedPlaceholder = "__server_managed__",
|
|
319
|
+
debug = false
|
|
320
|
+
} = options;
|
|
321
|
+
function log(msg) {
|
|
322
|
+
if (debug) console.debug(`[ServerBridgedStorage] ${msg}`);
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
get(key) {
|
|
326
|
+
if (key === STORAGE_KEYS.accessToken) {
|
|
327
|
+
const exp = readClientCookie(tokenExpirationCookie);
|
|
328
|
+
if (exp) {
|
|
329
|
+
const ms = parseInt(exp, 10);
|
|
330
|
+
if (!isNaN(ms) && ms > Date.now()) return serverManagedPlaceholder;
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
if (key === STORAGE_KEYS.refreshToken) {
|
|
335
|
+
const exp = readClientCookie(tokenExpirationCookie);
|
|
336
|
+
return exp ? serverManagedPlaceholder : null;
|
|
337
|
+
}
|
|
338
|
+
if (key === STORAGE_KEYS.idToken) return null;
|
|
339
|
+
if (key === STORAGE_KEYS.expiresAt) {
|
|
340
|
+
return readClientCookie(tokenExpirationCookie);
|
|
341
|
+
}
|
|
342
|
+
if (PKCE_KEYS.has(key)) {
|
|
343
|
+
return safeSessionStorage()?.getItem(key) ?? null;
|
|
344
|
+
}
|
|
345
|
+
return safeSessionStorage()?.getItem(key) ?? null;
|
|
346
|
+
},
|
|
347
|
+
set(key, value) {
|
|
348
|
+
if (SERVER_TOKEN_KEYS.has(key) || key === STORAGE_KEYS.expiresAt) {
|
|
349
|
+
log(`Ignoring set("${key}") \u2013 managed by server`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const ss = safeSessionStorage();
|
|
353
|
+
if (ss) {
|
|
354
|
+
try {
|
|
355
|
+
ss.setItem(key, value);
|
|
356
|
+
} catch (e) {
|
|
357
|
+
if (debug)
|
|
358
|
+
console.warn(
|
|
359
|
+
"[ServerBridgedStorage] sessionStorage.setItem failed:",
|
|
360
|
+
e
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
remove(key) {
|
|
366
|
+
if (SERVER_TOKEN_KEYS.has(key) || key === STORAGE_KEYS.expiresAt) {
|
|
367
|
+
log(`Ignoring remove("${key}") \u2013 managed by server`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const ss = safeSessionStorage();
|
|
371
|
+
if (ss) {
|
|
372
|
+
try {
|
|
373
|
+
ss.removeItem(key);
|
|
374
|
+
} catch (e) {
|
|
375
|
+
if (debug)
|
|
376
|
+
console.warn(
|
|
377
|
+
"[ServerBridgedStorage] sessionStorage.removeItem failed:",
|
|
378
|
+
e
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
clear() {
|
|
384
|
+
const ss = safeSessionStorage();
|
|
385
|
+
if (!ss) return;
|
|
386
|
+
for (const key of PKCE_KEYS) {
|
|
387
|
+
try {
|
|
388
|
+
ss.removeItem(key);
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
log("PKCE ephemeral data cleared from sessionStorage");
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
289
396
|
|
|
290
397
|
// src/client.ts
|
|
291
398
|
var DEFAULT_ISSUER = "https://auth.douvery.com";
|
|
@@ -303,6 +410,11 @@ var DouveryAuthClient = class {
|
|
|
303
410
|
error: null
|
|
304
411
|
};
|
|
305
412
|
constructor(config) {
|
|
413
|
+
if (!config) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
"[DouveryAuthClient] config is required. Make sure getDouveryAuthConfig() returns a valid DouveryAuthConfig object."
|
|
416
|
+
);
|
|
417
|
+
}
|
|
306
418
|
this.config = {
|
|
307
419
|
issuer: DEFAULT_ISSUER,
|
|
308
420
|
scopes: DEFAULT_SCOPES,
|
|
@@ -866,6 +978,7 @@ var DouveryAuthClient = class {
|
|
|
866
978
|
async getDiscovery() {
|
|
867
979
|
if (this.discovery) return this.discovery;
|
|
868
980
|
const discoveryUrl = `${this.config.issuer}/.well-known/openid-configuration`;
|
|
981
|
+
this.log("Fetching discovery from:", discoveryUrl);
|
|
869
982
|
const response = await fetch(discoveryUrl);
|
|
870
983
|
if (!response.ok) {
|
|
871
984
|
throw new AuthError(
|
|
@@ -873,7 +986,29 @@ var DouveryAuthClient = class {
|
|
|
873
986
|
"Failed to fetch discovery document"
|
|
874
987
|
);
|
|
875
988
|
}
|
|
876
|
-
|
|
989
|
+
const doc = await response.json();
|
|
990
|
+
const docIssuer = doc.issuer;
|
|
991
|
+
const configIssuer = this.config.issuer;
|
|
992
|
+
if (docIssuer && docIssuer !== configIssuer) {
|
|
993
|
+
this.log(
|
|
994
|
+
`Rewriting discovery endpoints: "${docIssuer}" -> "${configIssuer}"`
|
|
995
|
+
);
|
|
996
|
+
const rewrite = (url) => url.replace(docIssuer, configIssuer);
|
|
997
|
+
doc.issuer = configIssuer;
|
|
998
|
+
doc.authorization_endpoint = rewrite(doc.authorization_endpoint);
|
|
999
|
+
doc.token_endpoint = rewrite(doc.token_endpoint);
|
|
1000
|
+
if (doc.userinfo_endpoint)
|
|
1001
|
+
doc.userinfo_endpoint = rewrite(doc.userinfo_endpoint);
|
|
1002
|
+
if (doc.end_session_endpoint)
|
|
1003
|
+
doc.end_session_endpoint = rewrite(doc.end_session_endpoint);
|
|
1004
|
+
if (doc.jwks_uri) doc.jwks_uri = rewrite(doc.jwks_uri);
|
|
1005
|
+
if (doc.revocation_endpoint)
|
|
1006
|
+
doc.revocation_endpoint = rewrite(doc.revocation_endpoint);
|
|
1007
|
+
if (doc.introspection_endpoint)
|
|
1008
|
+
doc.introspection_endpoint = rewrite(doc.introspection_endpoint);
|
|
1009
|
+
}
|
|
1010
|
+
this.log("authorization_endpoint:", doc.authorization_endpoint);
|
|
1011
|
+
this.discovery = doc;
|
|
877
1012
|
return this.discovery;
|
|
878
1013
|
}
|
|
879
1014
|
setupAutoRefresh() {
|
|
@@ -931,6 +1066,6 @@ function createDouveryAuth(config) {
|
|
|
931
1066
|
return new DouveryAuthClient(config);
|
|
932
1067
|
}
|
|
933
1068
|
|
|
934
|
-
export { AuthError, CookieStorage, DouveryAuthClient, LocalStorage, MemoryStorage, STORAGE_KEYS, SessionStorage, TokenManager, base64UrlDecode, base64UrlEncode, createDouveryAuth, createStorage, decodeJWT, generateCodeChallenge, generateCodeVerifier, generateNonce, generatePKCEPair, generateState, getTokenExpiration, isTokenExpired, verifyCodeChallenge };
|
|
1069
|
+
export { AuthError, CookieStorage, DouveryAuthClient, LocalStorage, MemoryStorage, STORAGE_KEYS, SessionStorage, TokenManager, base64UrlDecode, base64UrlEncode, createDouveryAuth, createServerBridgedStorage, createStorage, decodeJWT, generateCodeChallenge, generateCodeVerifier, generateNonce, generatePKCEPair, generateState, getTokenExpiration, isTokenExpired, verifyCodeChallenge };
|
|
935
1070
|
//# sourceMappingURL=index.js.map
|
|
936
1071
|
//# sourceMappingURL=index.js.map
|