@~lyre/auth 0.11.1 → 0.12.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 CHANGED
@@ -1,3 +1,270 @@
1
+ import { KeyObject } from 'node:crypto';
2
+
3
+ type OidcErrorCode =
4
+ /** Client misconfiguration (e.g. client_secret_basic without a secret). */
5
+ 'invalid_config'
6
+ /** Callback `state` missing or not the one this app sent (possible CSRF). */
7
+ | 'state_mismatch'
8
+ /** Accounts redirected back with `?error=…` (e.g. access_denied). */
9
+ | 'authorization_error'
10
+ /** Callback carried no `code`. */
11
+ | 'missing_code'
12
+ /** Callback `iss` parameter (RFC 9207) is not our issuer. */
13
+ | 'issuer_mismatch'
14
+ /** Discovery document unreachable or invalid. */
15
+ | 'discovery_failed'
16
+ /** JWKS unreachable or invalid. */
17
+ | 'jwks_fetch_failed'
18
+ /** fetch() itself failed (DNS, connection refused, …). */
19
+ | 'network_error'
20
+ /** Token endpoint answered non-2xx (see `oauthError`, e.g. invalid_grant, invalid_client). */
21
+ | 'token_request_failed'
22
+ /** Token endpoint answered 2xx with an unusable body. */
23
+ | 'invalid_token_response'
24
+ /** `openid` was requested but no id_token came back. */
25
+ | 'missing_id_token'
26
+ /** Not a well-formed compact JWS. */
27
+ | 'invalid_jwt'
28
+ /** Header alg is not RS256 (blocks `none`, HS256 key confusion, …). */
29
+ | 'unsupported_alg'
30
+ /** No JWKS key matches the token's `kid`, even after a refetch. */
31
+ | 'unknown_kid' | 'invalid_signature' | 'audience_mismatch' | 'token_expired' | 'token_not_yet_valid' | 'nonce_mismatch'
32
+ /** A token that is not an access token (e.g. an id_token) was presented as one. */
33
+ | 'wrong_token_type'
34
+ /** userinfo answered non-2xx, or its `sub` disagrees with the token. */
35
+ | 'userinfo_failed' | 'revocation_failed';
36
+ declare class OidcError extends Error {
37
+ readonly code: OidcErrorCode;
38
+ /** HTTP status from Accounts, when the failure was an HTTP response. */
39
+ readonly status?: number;
40
+ /** OAuth `error` field (`invalid_grant`, `invalid_client`, `access_denied`, …). */
41
+ readonly oauthError?: string;
42
+ readonly oauthErrorDescription?: string;
43
+ constructor(code: OidcErrorCode, message: string, details?: {
44
+ status?: number;
45
+ oauthError?: string;
46
+ oauthErrorDescription?: string;
47
+ cause?: unknown;
48
+ });
49
+ }
50
+ declare function isOidcError(error: unknown, code?: OidcErrorCode): error is OidcError;
51
+ type OidcEndpoints = {
52
+ authorization: string;
53
+ token: string;
54
+ userinfo: string;
55
+ jwks: string;
56
+ revocation: string;
57
+ endSession: string;
58
+ };
59
+ /** Paths fixed by the contract, resolved against the issuer when discovery is off. */
60
+ declare const ACCOUNTS_ENDPOINT_PATHS: Readonly<OidcEndpoints>;
61
+ declare const DEFAULT_OIDC_SCOPES: readonly ["openid", "profile", "email", "offline_access"];
62
+ type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post' | 'none';
63
+ type CommonVerifierOptions = {
64
+ /** Issuer, e.g. `https://accounts.nipate.africa`. Tokens must carry exactly this `iss`. */
65
+ issuer: string;
66
+ /** Load endpoints from `/.well-known/openid-configuration` (cached). Default false: contract paths. */
67
+ discovery?: boolean;
68
+ /** Override individual endpoints (absolute URLs or paths relative to the issuer). */
69
+ endpoints?: Partial<OidcEndpoints>;
70
+ fetchImpl?: typeof fetch;
71
+ /** Allowed clock skew for exp/nbf, seconds. Default 30. */
72
+ clockToleranceSec?: number;
73
+ /** Discovery cache TTL. Default 1h. */
74
+ metadataCacheTtlMs?: number;
75
+ /** JWKS cache TTL. Default 1h. */
76
+ jwksCacheTtlMs?: number;
77
+ /** Minimum gap between JWKS refetches triggered by an unknown kid. Default 5s. */
78
+ jwksRefetchCooldownMs?: number;
79
+ /** Clock, epoch ms. For tests. */
80
+ now?: () => number;
81
+ };
82
+ type OidcClientOptions = CommonVerifierOptions & {
83
+ /** The app's client id (its app id in Accounts). Tokens must carry it as `aud`. */
84
+ clientId: string;
85
+ /** Confidential apps only. Public apps (browser-only, CLI) have none. */
86
+ clientSecret?: string;
87
+ /** Must match a registered redirect URI character for character. */
88
+ redirectUri: string;
89
+ /** Where Accounts sends the browser after `/auth/logout`. */
90
+ postLogoutRedirectUri?: string;
91
+ /** Default `openid profile email offline_access`. */
92
+ scopes?: readonly string[] | string;
93
+ /** Default `client_secret_basic` when a secret is set, otherwise `none`. */
94
+ tokenEndpointAuthMethod?: TokenEndpointAuthMethod;
95
+ };
96
+ type OidcLoginRequest = {
97
+ /** Send the browser here. */
98
+ url: string;
99
+ /** Keep these three server-side (or in a signed httpOnly cookie) until the callback. */
100
+ state: string;
101
+ nonce: string;
102
+ codeVerifier: string;
103
+ };
104
+ type OidcLoginExpectation = Pick<OidcLoginRequest, 'state' | 'nonce' | 'codeVerifier'>;
105
+ type TokenSet = {
106
+ accessToken: string;
107
+ tokenType: 'Bearer';
108
+ expiresIn: number;
109
+ /** Epoch ms at which the access token expires (local clock). */
110
+ expiresAt: number;
111
+ /** Rotates on every refresh: always store the new one; the old one stops working. */
112
+ refreshToken?: string;
113
+ idToken?: string;
114
+ scope?: string;
115
+ raw: Record<string, unknown>;
116
+ };
117
+ type IdTokenClaims = {
118
+ iss: string;
119
+ sub: string;
120
+ aud: string | string[];
121
+ exp: number;
122
+ iat: number;
123
+ auth_time?: number;
124
+ nonce?: string;
125
+ email?: string;
126
+ email_verified?: boolean;
127
+ name?: string;
128
+ given_name?: string;
129
+ family_name?: string;
130
+ picture?: string;
131
+ [claim: string]: unknown;
132
+ };
133
+ type AccessTokenClaims = {
134
+ iss: string;
135
+ sub: string;
136
+ aud: string | string[];
137
+ exp: number;
138
+ iat: number;
139
+ /** Tenant id. */
140
+ tid?: string | null;
141
+ /** Accounts session id. */
142
+ sid?: string;
143
+ /** Space-separated scopes. */
144
+ scope?: string;
145
+ jti?: string;
146
+ [claim: string]: unknown;
147
+ };
148
+ type UserInfo = {
149
+ sub: string;
150
+ email?: string;
151
+ email_verified?: boolean;
152
+ name?: string;
153
+ given_name?: string;
154
+ family_name?: string;
155
+ picture?: string;
156
+ [claim: string]: unknown;
157
+ };
158
+ type VerifiedAccessToken = {
159
+ claims: AccessTokenClaims;
160
+ /** `jwks`: verified locally. `userinfo`: JWKS unreachable, Accounts vouched for it remotely. */
161
+ verifiedBy: 'jwks' | 'userinfo';
162
+ };
163
+ declare function randomToken(bytes?: number): string;
164
+ /** RFC 7636 S256: BASE64URL(SHA256(ascii(verifier))). */
165
+ declare function pkceChallenge(verifier: string): string;
166
+ declare function createPkcePair(): {
167
+ codeVerifier: string;
168
+ codeChallenge: string;
169
+ };
170
+ type MetadataSource = {
171
+ get(): Promise<OidcEndpoints>;
172
+ };
173
+ declare class JwksStore {
174
+ private readonly metadata;
175
+ private readonly fetchImpl;
176
+ private readonly ttlMs;
177
+ private readonly cooldownMs;
178
+ private readonly now;
179
+ private keys;
180
+ private anonymous;
181
+ private fetchedAt;
182
+ private inflight;
183
+ /** Number of JWKS fetches performed (observability / tests). */
184
+ fetchCount: number;
185
+ constructor(metadata: MetadataSource, fetchImpl: typeof fetch, ttlMs: number, cooldownMs: number, now: () => number);
186
+ private refresh;
187
+ private lookup;
188
+ getKey(kid: string | undefined): Promise<KeyObject>;
189
+ }
190
+ /** True when the string has the shape of a compact JWS (three base64url segments). */
191
+ declare function looksLikeJwt(token: string): boolean;
192
+ type AccessTokenVerifierOptions = CommonVerifierOptions & {
193
+ /**
194
+ * Accepted `aud` values (client ids). Omit to accept a token issued to any app; the caller
195
+ * then reads `claims.aud` to see which app it was issued for.
196
+ */
197
+ audience?: string | readonly string[];
198
+ /**
199
+ * When the JWKS (or discovery) cannot be reached, ask Accounts' userinfo endpoint instead.
200
+ * Default true. Never used for a token that fails verification against a reachable JWKS.
201
+ */
202
+ remoteFallback?: boolean;
203
+ };
204
+ type AccessTokenVerifier = {
205
+ verify(token: string, options?: {
206
+ audience?: string | readonly string[];
207
+ }): Promise<VerifiedAccessToken>;
208
+ fetchUserInfo(accessToken: string): Promise<UserInfo>;
209
+ metadata(): Promise<OidcEndpoints>;
210
+ /** @internal */
211
+ readonly _jwks: JwksStore;
212
+ };
213
+ declare function createAccessTokenVerifier(opts: AccessTokenVerifierOptions): AccessTokenVerifier;
214
+ type OidcClient = {
215
+ readonly clientId: string;
216
+ readonly issuer: string;
217
+ readonly scope: string;
218
+ readonly authMethod: TokenEndpointAuthMethod;
219
+ metadata(): Promise<OidcEndpoints>;
220
+ /** Step 1: build the /auth/authorize URL. Keep `state`, `nonce`, `codeVerifier` until the callback. */
221
+ beginLogin(options?: {
222
+ scopes?: readonly string[] | string;
223
+ prompt?: string;
224
+ loginHint?: string;
225
+ extraParams?: Record<string, string>;
226
+ }): Promise<OidcLoginRequest>;
227
+ /** Steps 3–5: check state, exchange the code (with PKCE verifier), verify the id_token. */
228
+ handleCallback(input: {
229
+ /** The full callback URL, or just its query parameters. */
230
+ url?: string | URL;
231
+ params?: URLSearchParams | Record<string, string | undefined>;
232
+ expected: Partial<OidcLoginExpectation> | null | undefined;
233
+ }): Promise<{
234
+ claims: IdTokenClaims;
235
+ tokens: TokenSet;
236
+ }>;
237
+ /** Low-level code exchange. Does NOT verify the id_token: call `verifyIdToken(t, { nonce })` yourself. */
238
+ exchangeCode(input: {
239
+ code: string;
240
+ codeVerifier: string;
241
+ }): Promise<TokenSet>;
242
+ /** grant_type=refresh_token. The returned `refreshToken` replaces the old one, which is now dead. */
243
+ refresh(refreshToken: string, options?: {
244
+ scopes?: readonly string[] | string;
245
+ }): Promise<TokenSet>;
246
+ /** POST /api/auth/revoke. Throws OidcError('revocation_failed') on a non-2xx answer. */
247
+ revoke(token: string, tokenTypeHint?: 'refresh_token' | 'access_token'): Promise<void>;
248
+ verifyIdToken(idToken: string, options?: {
249
+ nonce?: string;
250
+ }): Promise<IdTokenClaims>;
251
+ /** Verify locally against the JWKS (aud defaults to this client id); userinfo fallback if the JWKS is down. */
252
+ verifyAccessToken(token: string, options?: {
253
+ audience?: string | readonly string[];
254
+ remoteFallback?: boolean;
255
+ }): Promise<VerifiedAccessToken>;
256
+ fetchUserInfo(accessToken: string): Promise<UserInfo>;
257
+ /** /auth/logout?client_id=…&post_logout_redirect_uri=… */
258
+ logoutUrl(options?: {
259
+ postLogoutRedirectUri?: string;
260
+ state?: string;
261
+ idTokenHint?: string;
262
+ }): Promise<string>;
263
+ /** @internal */
264
+ readonly _jwks: JwksStore;
265
+ };
266
+ declare function createOidcClient(opts: OidcClientOptions): OidcClient;
267
+
1
268
  type PermissionSet = string[];
2
269
  type AccountsIdentity = {
3
270
  id: string;
@@ -49,18 +316,29 @@ type PlatformSession = {
49
316
  expiresAt?: string;
50
317
  };
51
318
  type AccountsClientConfig = {
319
+ /** Accounts origin, e.g. `https://accounts.nipate.africa`. */
52
320
  baseUrl?: string;
321
+ /** OIDC issuer. Defaults to `baseUrl`. Added 0.12.0. */
322
+ issuer?: string;
53
323
  clientId: string;
324
+ /** Confidential apps only; public apps leave it unset. */
54
325
  clientSecret?: string;
55
326
  redirectUri: string;
327
+ /** Absolute URL Accounts returns the browser to after `/auth/logout`. */
56
328
  logoutRedirectUri?: string;
329
+ /** Requested scopes. Default `openid profile email offline_access`. Added 0.12.0. */
330
+ scopes?: string[];
331
+ /** Load endpoints from `/.well-known/openid-configuration`. Default false. Added 0.12.0. */
332
+ discovery?: boolean;
57
333
  useMock?: boolean;
58
334
  };
335
+ /** @deprecated Since 0.12.0; legacy login state. See `OidcLoginRequest`. */
59
336
  type AuthorizationState = {
60
337
  nonce: string;
61
338
  nextPath: string;
62
339
  tenantSlug?: string;
63
340
  };
341
+ /** @deprecated Since 0.12.0; legacy token response with a `user`. See `TokenSet` + `IdTokenClaims`. */
64
342
  type AccountsTokenResponse = {
65
343
  access_token: string;
66
344
  refresh_token?: string;
@@ -86,10 +364,22 @@ type SyncAccountsUserResult = {
86
364
  };
87
365
  type SyncAccountsUser = (identity: AccountsIdentity) => SyncAccountsUserResult | Promise<SyncAccountsUserResult>;
88
366
  declare function createAccountsClientConfig(input?: Partial<AccountsClientConfig>): AccountsClientConfig;
367
+ /**
368
+ * Build an OIDC client from an AccountsClientConfig (issuer falls back to baseUrl). Added 0.12.0.
369
+ */
370
+ declare function createOidcClientFromConfig(config: AccountsClientConfig, overrides?: Partial<OidcClientOptions>): OidcClient;
371
+ /** Map verified id_token (and optional userinfo) claims to an AccountsIdentity. Added 0.12.0. */
372
+ declare function identityFromClaims(claims: IdTokenClaims | UserInfo, userInfo?: UserInfo): AccountsIdentity;
373
+ /** Only same-origin absolute paths: `/x` yes, `//evil.example` and `/\\evil` no. */
374
+ declare function safeNextPath(value: string | null | undefined): string;
89
375
  declare function createPlatformAuth(): {
90
376
  readSession(cookieHeader: string | null | undefined): PlatformSession;
91
377
  requirePrincipal(session: PlatformSession): AuthenticatedPrincipal;
92
378
  };
379
+ /**
380
+ * @deprecated Since 0.12.0. Sends no PKCE, nonce or scope, which Accounts now refuses. Use
381
+ * `createOidcClient().beginLogin()` (+ `createLoginStateCookie`). Still used for mock mode.
382
+ */
93
383
  declare function beginAccountsLoginRedirect(config: AccountsClientConfig, options: {
94
384
  nextPath?: string;
95
385
  tenantSlug?: string;
@@ -99,6 +389,10 @@ declare function beginAccountsLoginRedirect(config: AccountsClientConfig, option
99
389
  stateValue: string;
100
390
  stateCookie: string;
101
391
  };
392
+ /**
393
+ * @deprecated Since 0.12.0. Trusts the token response's `user` instead of a verified id_token. Use
394
+ * `createOidcClient().handleCallback()` (+ `readLoginStateCookie`). Still used for mock mode.
395
+ */
102
396
  declare function handleAccountsCallback(input: {
103
397
  config: AccountsClientConfig;
104
398
  code: string;
@@ -116,6 +410,10 @@ declare function handleAccountsCallback(input: {
116
410
  sessionCookie: string;
117
411
  clearStateCookie: string;
118
412
  }>;
413
+ /**
414
+ * @deprecated Since 0.12.0. JSON body, no PKCE verifier, no client authentication. Use
415
+ * `createOidcClient().exchangeCode()` or `.handleCallback()`.
416
+ */
119
417
  declare function exchangeAuthorizationCode(input: {
120
418
  config: AccountsClientConfig;
121
419
  code: string;
@@ -142,6 +440,26 @@ type CookieOptions = {
142
440
  secure?: boolean;
143
441
  };
144
442
  declare function clearPlatformSessionCookie(opts?: CookieOptions): string;
443
+ /** Set-Cookie string for a PlatformSession (HMAC-signed when `secret` is set). Added 0.12.0. */
444
+ declare function createPlatformSessionCookie(session: PlatformSession, opts?: CookieOptions, secret?: string): string;
445
+ declare const LOGIN_STATE_COOKIE_NAME = "accounts_oidc_login";
446
+ type StoredLogin = OidcLoginExpectation & {
447
+ nextPath: string;
448
+ tenantSlug?: string;
449
+ /** Epoch seconds when the login started. */
450
+ issuedAt: number;
451
+ };
452
+ declare function createLoginStateCookie(login: OidcLoginRequest | OidcLoginExpectation, options?: {
453
+ nextPath?: string;
454
+ tenantSlug?: string;
455
+ secret?: string;
456
+ cookieOptions?: CookieOptions;
457
+ }): string;
458
+ /** Read (and, with a secret, verify) the login state cookie. Null when absent, forged or older than 10 minutes. */
459
+ declare function readLoginStateCookie(cookieHeader: string | null | undefined, options?: {
460
+ secret?: string;
461
+ }): StoredLogin | null;
462
+ declare function clearLoginStateCookie(opts?: CookieOptions): string;
145
463
  type ServiceKeyApp = {
146
464
  id: string;
147
465
  slug: string;
@@ -162,6 +480,10 @@ declare function introspectServiceKey(input: {
162
480
  key: string;
163
481
  fetchImpl?: typeof fetch;
164
482
  }): Promise<ServiceKeyContext | null>;
483
+ /**
484
+ * @deprecated Since 0.12.0. The contract signs out by revoking the refresh token
485
+ * (`OidcClient.revoke`) and optionally sending the browser to `OidcClient.logoutUrl()`.
486
+ */
165
487
  declare function logoutFromAccounts(input: {
166
488
  config: Pick<AccountsClientConfig, 'baseUrl'>;
167
489
  accessToken: string;
@@ -188,4 +510,4 @@ type AccountsWebhookEvent = {
188
510
  declare function normalizeAccountsWebhook(payload: unknown): AccountsWebhookEvent;
189
511
  declare function scopeSatisfied(granted: string[] | undefined, required: string): boolean;
190
512
 
191
- export { type AccountsClientConfig, type AccountsIdentity, type AccountsTokenResponse, type AccountsWebhookEvent, type AuthenticatedPrincipal, type AuthorizationState, type CookieOptions, type LocalUser, type PermissionSet, type PlatformSession, type ServiceKeyApp, type ServiceKeyContext, type SyncAccountsUser, type SyncAccountsUserResult, type TenantAccessContext, type TenantContext, type TenantMembership, beginAccountsLoginRedirect, clearPlatformSessionCookie, createAccountsClientConfig, createPlatformAuth, exchangeAuthorizationCode, handleAccountsCallback, identityPassthroughSync, introspectServiceKey, logoutFromAccounts, normalizeAccountsWebhook, readPlatformSessionCookie, resolveActiveTenant, scopeSatisfied, serviceKeyFromRequest, syncAccountsUser, verifyWebhookSignature };
513
+ export { ACCOUNTS_ENDPOINT_PATHS, type AccessTokenClaims, type AccessTokenVerifier, type AccessTokenVerifierOptions, type AccountsClientConfig, type AccountsIdentity, type AccountsTokenResponse, type AccountsWebhookEvent, type AuthenticatedPrincipal, type AuthorizationState, type CookieOptions, DEFAULT_OIDC_SCOPES, type IdTokenClaims, LOGIN_STATE_COOKIE_NAME, type LocalUser, type OidcClient, type OidcClientOptions, type OidcEndpoints, OidcError, type OidcErrorCode, type OidcLoginExpectation, type OidcLoginRequest, type PermissionSet, type PlatformSession, type ServiceKeyApp, type ServiceKeyContext, type StoredLogin, type SyncAccountsUser, type SyncAccountsUserResult, type TenantAccessContext, type TenantContext, type TenantMembership, type TokenEndpointAuthMethod, type TokenSet, type UserInfo, type VerifiedAccessToken, beginAccountsLoginRedirect, clearLoginStateCookie, clearPlatformSessionCookie, createAccessTokenVerifier, createAccountsClientConfig, createLoginStateCookie, createOidcClient, createOidcClientFromConfig, createPkcePair, createPlatformAuth, createPlatformSessionCookie, exchangeAuthorizationCode, handleAccountsCallback, identityFromClaims, identityPassthroughSync, introspectServiceKey, isOidcError, logoutFromAccounts, looksLikeJwt, normalizeAccountsWebhook, pkceChallenge, randomToken, readLoginStateCookie, readPlatformSessionCookie, resolveActiveTenant, safeNextPath, scopeSatisfied, serviceKeyFromRequest, syncAccountsUser, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -1,34 +1,70 @@
1
1
  import {
2
+ ACCOUNTS_ENDPOINT_PATHS,
3
+ DEFAULT_OIDC_SCOPES,
4
+ LOGIN_STATE_COOKIE_NAME,
5
+ OidcError,
2
6
  beginAccountsLoginRedirect,
7
+ clearLoginStateCookie,
3
8
  clearPlatformSessionCookie,
9
+ createAccessTokenVerifier,
4
10
  createAccountsClientConfig,
11
+ createLoginStateCookie,
12
+ createOidcClient,
13
+ createOidcClientFromConfig,
14
+ createPkcePair,
5
15
  createPlatformAuth,
16
+ createPlatformSessionCookie,
6
17
  exchangeAuthorizationCode,
7
18
  handleAccountsCallback,
19
+ identityFromClaims,
8
20
  identityPassthroughSync,
9
21
  introspectServiceKey,
22
+ isOidcError,
10
23
  logoutFromAccounts,
24
+ looksLikeJwt,
11
25
  normalizeAccountsWebhook,
26
+ pkceChallenge,
27
+ randomToken,
28
+ readLoginStateCookie,
12
29
  readPlatformSessionCookie,
13
30
  resolveActiveTenant,
31
+ safeNextPath,
14
32
  scopeSatisfied,
15
33
  serviceKeyFromRequest,
16
34
  syncAccountsUser,
17
35
  verifyWebhookSignature
18
- } from "./chunk-MJ74QDOO.js";
36
+ } from "./chunk-F2GDXTY4.js";
19
37
  export {
38
+ ACCOUNTS_ENDPOINT_PATHS,
39
+ DEFAULT_OIDC_SCOPES,
40
+ LOGIN_STATE_COOKIE_NAME,
41
+ OidcError,
20
42
  beginAccountsLoginRedirect,
43
+ clearLoginStateCookie,
21
44
  clearPlatformSessionCookie,
45
+ createAccessTokenVerifier,
22
46
  createAccountsClientConfig,
47
+ createLoginStateCookie,
48
+ createOidcClient,
49
+ createOidcClientFromConfig,
50
+ createPkcePair,
23
51
  createPlatformAuth,
52
+ createPlatformSessionCookie,
24
53
  exchangeAuthorizationCode,
25
54
  handleAccountsCallback,
55
+ identityFromClaims,
26
56
  identityPassthroughSync,
27
57
  introspectServiceKey,
58
+ isOidcError,
28
59
  logoutFromAccounts,
60
+ looksLikeJwt,
29
61
  normalizeAccountsWebhook,
62
+ pkceChallenge,
63
+ randomToken,
64
+ readLoginStateCookie,
30
65
  readPlatformSessionCookie,
31
66
  resolveActiveTenant,
67
+ safeNextPath,
32
68
  scopeSatisfied,
33
69
  serviceKeyFromRequest,
34
70
  syncAccountsUser,
package/dist/nest.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { AccountsWebhookEvent, ServiceKeyContext, AccountsClientConfig } from './index.js';
1
+ import { AccountsWebhookEvent, ServiceKeyContext, AccountsClientConfig, AccessTokenVerifierOptions } from './index.js';
2
2
  export { normalizeAccountsWebhook } from './index.js';
3
+ import 'node:crypto';
3
4
 
4
5
  /**
5
6
  * The minimum a request must expose. Fastify's `request` satisfies this as-is
@@ -37,17 +38,34 @@ type UserSessionContext = {
37
38
  userId: string;
38
39
  email?: string;
39
40
  name?: string;
41
+ /** Accounts session id (`sid`). Empty when the token carries none. */
40
42
  sessionId: string;
41
- /** The app the session was issued for. Null for sessions created outside an app context. */
43
+ /** The app (client id) the token was issued for: the JWT `aud`. */
42
44
  appId: string | null;
43
45
  tenantId: string | null;
44
46
  expiresAt?: string;
47
+ /** Scopes granted to the token (JWT `scope`). Added 0.12.0. */
48
+ scopes?: string[];
49
+ /** How the token was checked. Added 0.12.0. */
50
+ verifiedBy?: 'jwks' | 'userinfo' | 'legacy-me';
45
51
  };
46
52
  type UserSessionResolverOptions = {
47
- config: Pick<AccountsClientConfig, 'baseUrl'>;
53
+ /** `baseUrl` is the Accounts origin; `issuer` defaults to it. */
54
+ config: Pick<AccountsClientConfig, 'baseUrl'> & Partial<Pick<AccountsClientConfig, 'issuer' | 'discovery'>>;
48
55
  fetchImpl?: typeof fetch;
49
- /** Cache TTL in ms. Default 60_000; 0 disables caching. */
56
+ /** Cache TTL in ms. Default 60_000; 0 disables caching. Never outlives the token's exp. */
50
57
  cacheTtlMs?: number;
58
+ /**
59
+ * Accept only tokens issued to these client ids (`aud`). Strongly recommended: without it a
60
+ * token issued to ANY Nipate app is accepted and the caller must check `appId` itself.
61
+ */
62
+ audience?: string | readonly string[];
63
+ /** Ask userinfo when the JWKS is unreachable. Default true. */
64
+ remoteFallback?: boolean;
65
+ /** Check opaque (pre-JWT) bearers via GET /api/auth/me. Default true; turn off once Accounts issues JWTs. */
66
+ legacySessionTokens?: boolean;
67
+ /** Extra verifier options (endpoints, clock tolerance, JWKS cache TTL/cooldown, clock). */
68
+ verifier?: Partial<Omit<AccessTokenVerifierOptions, 'issuer' | 'audience' | 'fetchImpl'>>;
51
69
  };
52
70
  /** A session bearer is any Authorization: Bearer token that is NOT an `sk_` service key. */
53
71
  declare function sessionTokenFromHeaders(carrier: HeaderCarrier): string | null;
package/dist/nest.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import {
2
+ createAccessTokenVerifier,
2
3
  introspectServiceKey,
4
+ looksLikeJwt,
3
5
  normalizeAccountsWebhook,
4
6
  scopeSatisfied,
5
7
  serviceKeyFromRequest,
6
8
  verifyWebhookSignature
7
- } from "./chunk-MJ74QDOO.js";
9
+ } from "./chunk-F2GDXTY4.js";
8
10
 
9
11
  // src/nest.ts
10
12
  import { createHash } from "crypto";
@@ -60,9 +62,42 @@ function sessionTokenFromHeaders(carrier) {
60
62
  function createUserSessionResolver(opts) {
61
63
  const ttl = opts.cacheTtlMs ?? 6e4;
62
64
  const cache = /* @__PURE__ */ new Map();
63
- const fetchImpl = opts.fetchImpl ?? fetch;
64
- async function introspect(token) {
65
- const base = opts.config.baseUrl?.replace(/\/$/, "");
65
+ const fetchImpl = opts.fetchImpl ?? ((...args) => fetch(...args));
66
+ const base = opts.config.baseUrl?.trim().replace(/\/$/, "");
67
+ const issuer = (opts.config.issuer ?? base)?.trim();
68
+ const legacy = opts.legacySessionTokens ?? true;
69
+ let verifier = null;
70
+ if (issuer) {
71
+ verifier = createAccessTokenVerifier({
72
+ discovery: opts.config.discovery,
73
+ ...opts.verifier,
74
+ issuer,
75
+ audience: opts.audience,
76
+ remoteFallback: opts.remoteFallback ?? true,
77
+ fetchImpl
78
+ });
79
+ }
80
+ async function viaJwt(token) {
81
+ if (!verifier) return null;
82
+ try {
83
+ const { claims, verifiedBy } = await verifier.verify(token);
84
+ const aud = Array.isArray(claims.aud) ? claims.aud[0] : claims.aud;
85
+ return {
86
+ userId: claims.sub,
87
+ email: typeof claims.email === "string" ? claims.email : void 0,
88
+ name: typeof claims.name === "string" ? claims.name : void 0,
89
+ sessionId: claims.sid ?? "",
90
+ appId: aud ?? null,
91
+ tenantId: claims.tid ?? null,
92
+ expiresAt: new Date(claims.exp * 1e3).toISOString(),
93
+ scopes: claims.scope ? claims.scope.split(" ").filter(Boolean) : [],
94
+ verifiedBy
95
+ };
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ async function viaLegacyMe(token) {
66
101
  if (!base || !token) return null;
67
102
  try {
68
103
  const res = await fetchImpl(new URL("/api/auth/me", base), {
@@ -73,19 +108,27 @@ function createUserSessionResolver(opts) {
73
108
  const userId = data.user?.id;
74
109
  const sessionId = data.session?.id;
75
110
  if (!userId || !sessionId) return null;
111
+ const appId = data.session?.appId ?? null;
112
+ const allowed = opts.audience === void 0 ? null : typeof opts.audience === "string" ? [opts.audience] : opts.audience;
113
+ if (allowed && (!appId || !allowed.includes(appId))) return null;
76
114
  return {
77
115
  userId,
78
116
  email: data.user?.emailAddress ?? data.user?.email,
79
117
  name: data.user?.name,
80
118
  sessionId,
81
- appId: data.session?.appId ?? null,
119
+ appId,
82
120
  tenantId: data.session?.tenantId ?? null,
83
- expiresAt: data.session?.expiresAt
121
+ expiresAt: data.session?.expiresAt,
122
+ verifiedBy: "legacy-me"
84
123
  };
85
124
  } catch {
86
125
  return null;
87
126
  }
88
127
  }
128
+ async function introspect(token) {
129
+ if (looksLikeJwt(token)) return viaJwt(token);
130
+ return legacy ? viaLegacyMe(token) : null;
131
+ }
89
132
  async function resolve(carrier) {
90
133
  const token = sessionTokenFromHeaders(carrier);
91
134
  if (!token) return null;
@@ -94,7 +137,10 @@ function createUserSessionResolver(opts) {
94
137
  const cached = ttl > 0 ? cache.get(hash) : void 0;
95
138
  if (cached && cached.expires > now) return cached.value;
96
139
  const ctx = await introspect(token);
97
- if (ttl > 0) cache.set(hash, { value: ctx, expires: now + ttl });
140
+ if (ttl > 0) {
141
+ const exp = ctx?.expiresAt ? Date.parse(ctx.expiresAt) : Infinity;
142
+ cache.set(hash, { value: ctx, expires: Math.min(now + ttl, exp) });
143
+ }
98
144
  return ctx;
99
145
  }
100
146
  function invalidate(token) {
@@ -1,6 +1,7 @@
1
1
  import { RequestEvent, Handle } from '@sveltejs/kit';
2
- import { AccountsWebhookEvent, PlatformSession, AccountsClientConfig, ServiceKeyContext, SyncAccountsUser, CookieOptions } from './index.js';
2
+ import { AccountsWebhookEvent, PlatformSession, AccountsClientConfig, ServiceKeyContext, SyncAccountsUser, CookieOptions, OidcClientOptions, OidcClient } from './index.js';
3
3
  export { clearPlatformSessionCookie } from './index.js';
4
+ import 'node:crypto';
4
5
 
5
6
  type SvelteKitAuthOptions = {
6
7
  /** Accounts client config (use createAccountsClientConfig()). */
@@ -29,6 +30,23 @@ type SvelteKitAuthOptions = {
29
30
  * recommended in production, since the session payload includes tenant memberships.
30
31
  */
31
32
  sessionSecret?: string;
33
+ /** OIDC client overrides (issuer, scopes, discovery, endpoints, tokenEndpointAuthMethod…). Added 0.12.0. */
34
+ oidc?: Partial<OidcClientOptions>;
35
+ /** A prebuilt client; takes precedence over `config` + `oidc`. Added 0.12.0. */
36
+ client?: OidcClient;
37
+ fetchImpl?: typeof fetch;
38
+ /**
39
+ * Refresh the Accounts access token when it is within 60s of expiry and the session holds a
40
+ * refresh token. Default true. A refused refresh (reused/revoked token) logs the session out.
41
+ */
42
+ refreshTokens?: boolean;
43
+ /**
44
+ * After local sign-out, send the browser to Accounts' `/auth/logout` to end the Accounts session
45
+ * too (returning to `config.logoutRedirectUri`). Default false: only this app signs out.
46
+ */
47
+ endAccountsSession?: boolean;
48
+ /** Render a failed callback. Default: a plain 400 naming the OidcError code. */
49
+ onAuthError?: (error: unknown, event: RequestEvent) => Response | Promise<Response>;
32
50
  };
33
51
  type PlatformLocals = {
34
52
  session: PlatformSession;