@nexartis/sentinel-sdk 1.7.0 → 1.8.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/README.md +37 -2
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/jwks.d.ts +76 -0
- package/dist/core/jwks.d.ts.map +1 -0
- package/dist/core/jwks.js +231 -0
- package/dist/core/jwks.js.map +1 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/sveltekit/client/index.d.ts.map +1 -1
- package/dist/sveltekit/client/index.js +31 -9
- package/dist/sveltekit/client/index.js.map +1 -1
- package/dist/sveltekit/handle.d.ts +5 -2
- package/dist/sveltekit/handle.d.ts.map +1 -1
- package/dist/sveltekit/handle.js +123 -10
- package/dist/sveltekit/handle.js.map +1 -1
- package/dist/sveltekit/handlers/index.d.ts.map +1 -1
- package/dist/sveltekit/handlers/index.js +7 -1
- package/dist/sveltekit/handlers/index.js.map +1 -1
- package/dist/sveltekit/utils.d.ts.map +1 -1
- package/dist/sveltekit/utils.js +10 -2
- package/dist/sveltekit/utils.js.map +1 -1
- package/package.json +87 -97
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Sentinel is a passwordless authentication service: users sign in with a one-time
|
|
|
13
13
|
## Table of contents
|
|
14
14
|
|
|
15
15
|
- [What you get](#what-you-get)
|
|
16
|
+
- [Security: signature verification](#security-signature-verification)
|
|
16
17
|
- [Install](#install)
|
|
17
18
|
- [Configure](#configure)
|
|
18
19
|
- [Quickstart](#quickstart)
|
|
@@ -37,6 +38,40 @@ Sentinel is a passwordless authentication service: users sign in with a one-time
|
|
|
37
38
|
|
|
38
39
|
---
|
|
39
40
|
|
|
41
|
+
## Security: signature verification
|
|
42
|
+
|
|
43
|
+
Since **1.8.0**, the SvelteKit handle verifies every access token's **Ed25519
|
|
44
|
+
(EdDSA) signature** against the auth server's published JWKS before trusting
|
|
45
|
+
the payload. A forged or tampered cookie is treated as unauthenticated — it
|
|
46
|
+
can no longer inject arbitrary `locals.user.roles` (e.g. `admin`) into
|
|
47
|
+
downstream authorization gates.
|
|
48
|
+
|
|
49
|
+
- JWKS is fetched once and cached per isolate (default TTL **10 minutes**;
|
|
50
|
+
override with `jwksCacheTtlMs`). An unknown `kid` triggers one throttled
|
|
51
|
+
refresh to pick up rotated keys.
|
|
52
|
+
- Only `alg: EdDSA` is accepted. `HS256`, `none`, and any other algorithm are
|
|
53
|
+
rejected outright.
|
|
54
|
+
- Verification **never throws**. If the JWKS document is unreachable the
|
|
55
|
+
request fails closed to unauthenticated (not a 500).
|
|
56
|
+
|
|
57
|
+
The JWKS URL defaults to `<apiUrl origin>/.well-known/jwks.json`; override it
|
|
58
|
+
with `jwksUrl` or the `SENTINEL_JWKS_URL` environment variable.
|
|
59
|
+
|
|
60
|
+
> **Backend requirement:** the Sentinel service must expose Ed25519 keys and
|
|
61
|
+
> sign tokens with EdDSA. Legacy `HS256` tokens are rejected — complete the
|
|
62
|
+
> backend Ed25519 migration before upgrading to this SDK version.
|
|
63
|
+
|
|
64
|
+
### Development-only opt-out
|
|
65
|
+
|
|
66
|
+
Setting `SENTINEL_SKIP_JWKS_VERIFY=1` (or `skipJwksVerify: true`) restores the
|
|
67
|
+
pre-1.8.0 behaviour of decoding the payload without verifying the signature.
|
|
68
|
+
This is an escape hatch for local development against a backend that has not
|
|
69
|
+
yet been migrated. It logs a loud warning per isolate and **must never be
|
|
70
|
+
enabled in production** — it reopens the exact unverified-payload hole this
|
|
71
|
+
release closes.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
40
75
|
## Install
|
|
41
76
|
|
|
42
77
|
```bash
|
|
@@ -64,7 +99,7 @@ The SDK does not bake in any backend URL. Every request is directed at the URL *
|
|
|
64
99
|
| `apiUrl` | Base URL of the Sentinel auth backend (e.g. `https://auth.example.com`). |
|
|
65
100
|
| `baseUrl` | Public base URL of your own app (e.g. `https://app.example.com`) — used when constructing magic-link callback URLs. |
|
|
66
101
|
|
|
67
|
-
Expose them via SvelteKit environment (`$env/static/private` for `apiUrl`, `$env/static/public` for the browser-visible base URL) and pass them into `createSentinelHandle` as top-level `apiUrl` / `baseUrl` options. `createAuthHandlers` reads
|
|
102
|
+
Expose them via SvelteKit environment (`$env/static/private` for `apiUrl`, `$env/static/public` for the browser-visible base URL) and pass them into `createSentinelHandle` as top-level `apiUrl` / `baseUrl` options. `createAuthHandlers` reads `SENTINEL_API_URL` plus `VITE_BASE_URL` or `PUBLIC_BASE_URL` from `event.platform.env` (Workers) and falls back to the same process env names on Node / `adapter-node`.
|
|
68
103
|
|
|
69
104
|
```bash
|
|
70
105
|
# .env
|
|
@@ -172,7 +207,7 @@ const client = new SentinelClient({
|
|
|
172
207
|
|
|
173
208
|
const decoded = decodeTokenLocally(accessToken);
|
|
174
209
|
if (isTokenExpired(accessToken)) {
|
|
175
|
-
|
|
210
|
+
const refreshed = await client.refreshToken(refreshToken);
|
|
176
211
|
}
|
|
177
212
|
```
|
|
178
213
|
|
package/dist/core/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { SentinelClient } from './client.js';
|
|
|
2
2
|
export { SentinelError, SentinelTimeoutError, ErrorCode, SentinelCode, isRetryableError, isRetryableStatus } from './errors.js';
|
|
3
3
|
export type { SentinelCodeValue } from './errors.js';
|
|
4
4
|
export { validateToken, isTokenExpired, decodeTokenLocally } from './auth-utils.js';
|
|
5
|
+
export { createJwksVerifier, __resetJwksCacheForTests } from './jwks.js';
|
|
6
|
+
export type { JwksVerifier, JwksVerifierOptions, TokenVerifyOutcome } from './jwks.js';
|
|
5
7
|
export { createLogger, noopLogger } from './logger.js';
|
|
6
8
|
export type { SentinelLogger, LogLevel, LogEntry } from './logger.js';
|
|
7
9
|
export type { SentinelConfig, SentinelResponse, SentinelResponseBase, StartLoginResponse, PollResponse, ClaimResponse, RefreshResponse, ValidateResponse, RegisterResponse, VerifyResponse, CompletePasswordlessResponse, DecodedToken, UserLocals, SentinelHandleOptions, AuthHandlerOptions, BeforeLoginContext, BeforeLoginReject, AfterLoginContext, AfterClaimContext, ErrorContext, ResolveRedirectContext, LogoutRedirectContext } from './types.js';
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,SAAS,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChI,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACtE,YAAY,EACX,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,4BAA4B,EAC5B,YAAY,EACZ,UAAU,EACV,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EACrB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,SAAS,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChI,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AACzE,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACtE,YAAY,EACX,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,4BAA4B,EAC5B,YAAY,EACZ,UAAU,EACV,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EACrB,MAAM,YAAY,CAAC"}
|
package/dist/core/index.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
export { SentinelClient } from './client.js';
|
|
3
3
|
export { SentinelError, SentinelTimeoutError, ErrorCode, SentinelCode, isRetryableError, isRetryableStatus } from './errors.js';
|
|
4
4
|
export { validateToken, isTokenExpired, decodeTokenLocally } from './auth-utils.js';
|
|
5
|
+
export { createJwksVerifier, __resetJwksCacheForTests } from './jwks.js';
|
|
5
6
|
export { createLogger, noopLogger } from './logger.js';
|
|
6
7
|
//# sourceMappingURL=index.js.map
|
package/dist/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,SAAS,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhI,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC","sourcesContent":["// src/core/index.ts\nexport { SentinelClient } from './client.js';\nexport { SentinelError, SentinelTimeoutError, ErrorCode, SentinelCode, isRetryableError, isRetryableStatus } from './errors.js';\nexport type { SentinelCodeValue } from './errors.js';\nexport { validateToken, isTokenExpired, decodeTokenLocally } from './auth-utils.js';\nexport { createLogger, noopLogger } from './logger.js';\nexport type { SentinelLogger, LogLevel, LogEntry } from './logger.js';\nexport type {\n\tSentinelConfig,\n\tSentinelResponse,\n\tSentinelResponseBase,\n\tStartLoginResponse,\n\tPollResponse,\n\tClaimResponse,\n\tRefreshResponse,\n\tValidateResponse,\n\tRegisterResponse,\n\tVerifyResponse,\n\tCompletePasswordlessResponse,\n\tDecodedToken,\n\tUserLocals,\n\tSentinelHandleOptions,\n\tAuthHandlerOptions,\n\tBeforeLoginContext,\n\tBeforeLoginReject,\n\tAfterLoginContext,\n\tAfterClaimContext,\n\tErrorContext,\n\tResolveRedirectContext,\n\tLogoutRedirectContext\n} from './types.js';\n\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,SAAS,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhI,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC","sourcesContent":["// src/core/index.ts\nexport { SentinelClient } from './client.js';\nexport { SentinelError, SentinelTimeoutError, ErrorCode, SentinelCode, isRetryableError, isRetryableStatus } from './errors.js';\nexport type { SentinelCodeValue } from './errors.js';\nexport { validateToken, isTokenExpired, decodeTokenLocally } from './auth-utils.js';\nexport { createJwksVerifier, __resetJwksCacheForTests } from './jwks.js';\nexport type { JwksVerifier, JwksVerifierOptions, TokenVerifyOutcome } from './jwks.js';\nexport { createLogger, noopLogger } from './logger.js';\nexport type { SentinelLogger, LogLevel, LogEntry } from './logger.js';\nexport type {\n\tSentinelConfig,\n\tSentinelResponse,\n\tSentinelResponseBase,\n\tStartLoginResponse,\n\tPollResponse,\n\tClaimResponse,\n\tRefreshResponse,\n\tValidateResponse,\n\tRegisterResponse,\n\tVerifyResponse,\n\tCompletePasswordlessResponse,\n\tDecodedToken,\n\tUserLocals,\n\tSentinelHandleOptions,\n\tAuthHandlerOptions,\n\tBeforeLoginContext,\n\tBeforeLoginReject,\n\tAfterLoginContext,\n\tAfterClaimContext,\n\tErrorContext,\n\tResolveRedirectContext,\n\tLogoutRedirectContext\n} from './types.js';\n\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWKS-based access-token signature verification (Ed25519 / EdDSA).
|
|
3
|
+
*
|
|
4
|
+
* Security model: the Sentinel backend signs access tokens with EdDSA and
|
|
5
|
+
* publishes the public key at `/.well-known/jwks.json` (RFC 7517). This
|
|
6
|
+
* module fetches and caches that key set, then verifies each access token's
|
|
7
|
+
* signature with WebCrypto BEFORE the payload is trusted. Tokens that fail
|
|
8
|
+
* verification are treated as unauthenticated — verification never throws.
|
|
9
|
+
*
|
|
10
|
+
* Cache behaviour:
|
|
11
|
+
* - JWKS documents are cached per isolate, keyed by URL, with a TTL
|
|
12
|
+
* (default 10 minutes).
|
|
13
|
+
* - A token whose `kid` is not in the cache triggers ONE forced refresh
|
|
14
|
+
* (key rotation support), throttled to at most once per
|
|
15
|
+
* `refreshMinIntervalMs` (default 10 seconds) so unknown-kid floods cannot
|
|
16
|
+
* be used to hammer the auth server.
|
|
17
|
+
* - JWKS fetch failure classifies as `unavailable` (treat as
|
|
18
|
+
* unauthenticated); it never surfaces as a 500.
|
|
19
|
+
*
|
|
20
|
+
* Only OKP/Ed25519 keys with `alg: EdDSA` semantics are accepted. Tokens
|
|
21
|
+
* with any other algorithm (including legacy HS256) are rejected as
|
|
22
|
+
* `invalid` — a symmetric HS256 secret can never be verified from a public
|
|
23
|
+
* JWKS, and accepting it here would reintroduce the unverified-payload hole.
|
|
24
|
+
*/
|
|
25
|
+
import type { DecodedToken } from './types.js';
|
|
26
|
+
import type { SentinelLogger } from './logger.js';
|
|
27
|
+
export interface JwksVerifierOptions {
|
|
28
|
+
/** Full URL of the JWKS document, e.g. https://auth.example.com/.well-known/jwks.json */
|
|
29
|
+
jwksUrl: string;
|
|
30
|
+
/** Fetch implementation. Defaults to globalThis.fetch (pass event.fetch in SvelteKit handles). */
|
|
31
|
+
fetchFn?: typeof globalThis.fetch;
|
|
32
|
+
/** Cache TTL in milliseconds. Default: 600_000 (10 minutes). */
|
|
33
|
+
cacheTtlMs?: number;
|
|
34
|
+
/** Minimum interval between forced refreshes for unknown kids, in ms. Default: 10_000. */
|
|
35
|
+
refreshMinIntervalMs?: number;
|
|
36
|
+
/** Timeout for each JWKS fetch, in ms. Default: 5_000. */
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
/** Optional structured logger. */
|
|
39
|
+
logger?: SentinelLogger;
|
|
40
|
+
}
|
|
41
|
+
export type TokenVerifyOutcome =
|
|
42
|
+
/** Signature valid, payload structurally sound and not expired. */
|
|
43
|
+
{
|
|
44
|
+
status: 'valid';
|
|
45
|
+
payload: DecodedToken;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Token rejected: bad signature, malformed token, unsupported alg,
|
|
49
|
+
* unknown kid (after refresh), expired, or structurally invalid payload.
|
|
50
|
+
* Treat the request as unauthenticated.
|
|
51
|
+
*/
|
|
52
|
+
| {
|
|
53
|
+
status: 'invalid';
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Verification could not run because the JWKS document is unreachable or
|
|
58
|
+
* unusable. Treat the request as unauthenticated (fail closed) — never a 500.
|
|
59
|
+
*/
|
|
60
|
+
| {
|
|
61
|
+
status: 'unavailable';
|
|
62
|
+
reason: string;
|
|
63
|
+
};
|
|
64
|
+
export interface JwksVerifier {
|
|
65
|
+
verify(token: string): Promise<TokenVerifyOutcome>;
|
|
66
|
+
/** Test-only: drop cached JWKS state for this verifier's URL. */
|
|
67
|
+
_resetCacheForTests(): void;
|
|
68
|
+
}
|
|
69
|
+
/** Test-only: drop all cached JWKS state. */
|
|
70
|
+
export declare function __resetJwksCacheForTests(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Create a JWKS verifier bound to one JWKS URL. The underlying key cache is
|
|
73
|
+
* shared per isolate across verifiers with the same URL.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createJwksVerifier(userOptions: JwksVerifierOptions): JwksVerifier;
|
|
76
|
+
//# sourceMappingURL=jwks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../../src/core/jwks.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,WAAW,mBAAmB;IACnC,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,OAAO,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAClC,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kCAAkC;IAClC,MAAM,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,MAAM,kBAAkB;AAC7B,mEAAmE;AACjE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE;AAC5C;;;;GAIG;GACD;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACvC;;;GAGG;GACD;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACnD,iEAAiE;IACjE,mBAAmB,IAAI,IAAI,CAAC;CAC5B;AA2BD,6CAA6C;AAC7C,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C;AAoJD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,mBAAmB,GAAG,YAAY,CAgFjF"}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// src/core/jwks.ts
|
|
2
|
+
import { createLogger } from './logger.js';
|
|
3
|
+
// Per-isolate cache shared across verifiers with the same JWKS URL.
|
|
4
|
+
const jwksCache = new Map();
|
|
5
|
+
// Per-URL in-flight fetch dedup (avoids thundering herd on cold cache).
|
|
6
|
+
const inflightFetches = new Map();
|
|
7
|
+
// Per-URL throttle markers for forced refreshes on unknown kid.
|
|
8
|
+
const lastForcedRefreshAt = new Map();
|
|
9
|
+
/** Test-only: drop all cached JWKS state. */
|
|
10
|
+
export function __resetJwksCacheForTests() {
|
|
11
|
+
jwksCache.clear();
|
|
12
|
+
inflightFetches.clear();
|
|
13
|
+
lastForcedRefreshAt.clear();
|
|
14
|
+
}
|
|
15
|
+
function base64UrlDecodeToBytes(input) {
|
|
16
|
+
let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
17
|
+
const pad = base64.length % 4;
|
|
18
|
+
if (pad)
|
|
19
|
+
base64 += '='.repeat(4 - pad);
|
|
20
|
+
const binary = atob(base64);
|
|
21
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
22
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
23
|
+
bytes[i] = binary.charCodeAt(i);
|
|
24
|
+
}
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
function base64UrlDecodeToString(input) {
|
|
28
|
+
// Decode via Uint8Array + TextDecoder for proper UTF-8 handling.
|
|
29
|
+
return new TextDecoder('utf-8').decode(base64UrlDecodeToBytes(input));
|
|
30
|
+
}
|
|
31
|
+
async function importPublicEd25519Key(jwk) {
|
|
32
|
+
if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string' || jwk.x.length === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return await crypto.subtle.importKey('jwk', { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, { name: 'Ed25519' }, false, ['verify']);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function fetchJwksDocument(jwksUrl, fetchFn, timeoutMs, log) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetchFn(jwksUrl, {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
headers: { accept: 'application/json' }
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
log.warn('JWKS fetch returned non-OK status', { jwksUrl, status: response.status });
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const document = (await response.json());
|
|
55
|
+
if (!Array.isArray(document.keys)) {
|
|
56
|
+
log.warn('JWKS document has no keys array', { jwksUrl });
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const keys = new Map();
|
|
60
|
+
for (const rawKey of document.keys) {
|
|
61
|
+
if (typeof rawKey?.kid !== 'string' || rawKey.kid.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
const imported = await importPublicEd25519Key(rawKey);
|
|
64
|
+
if (imported)
|
|
65
|
+
keys.set(rawKey.kid, imported);
|
|
66
|
+
}
|
|
67
|
+
if (keys.size === 0) {
|
|
68
|
+
log.warn('JWKS document contained no usable Ed25519 keys', { jwksUrl });
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return { keys, fetchedAt: Date.now() };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
log.warn('JWKS fetch failed', { jwksUrl, error: String(error) });
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function getJwksEntry(options, force, log) {
|
|
82
|
+
const { jwksUrl, cacheTtlMs, refreshMinIntervalMs, timeoutMs } = options;
|
|
83
|
+
const fetchFn = options.fetchFn ?? globalThis.fetch;
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const cached = jwksCache.get(jwksUrl);
|
|
86
|
+
if (!force && cached && now - cached.fetchedAt < cacheTtlMs) {
|
|
87
|
+
return cached;
|
|
88
|
+
}
|
|
89
|
+
if (force) {
|
|
90
|
+
const lastForced = lastForcedRefreshAt.get(jwksUrl) ?? 0;
|
|
91
|
+
if (now - lastForced < refreshMinIntervalMs) {
|
|
92
|
+
// Throttled: report whatever we have (possibly nothing).
|
|
93
|
+
return cached ?? null;
|
|
94
|
+
}
|
|
95
|
+
lastForcedRefreshAt.set(jwksUrl, now);
|
|
96
|
+
}
|
|
97
|
+
const inflight = inflightFetches.get(jwksUrl);
|
|
98
|
+
if (inflight)
|
|
99
|
+
return inflight;
|
|
100
|
+
const pending = fetchJwksDocument(jwksUrl, fetchFn, timeoutMs, log)
|
|
101
|
+
.then((entry) => {
|
|
102
|
+
if (entry)
|
|
103
|
+
jwksCache.set(jwksUrl, entry);
|
|
104
|
+
return entry;
|
|
105
|
+
})
|
|
106
|
+
.finally(() => {
|
|
107
|
+
inflightFetches.delete(jwksUrl);
|
|
108
|
+
});
|
|
109
|
+
inflightFetches.set(jwksUrl, pending);
|
|
110
|
+
return pending;
|
|
111
|
+
}
|
|
112
|
+
function parseTokenHeader(token) {
|
|
113
|
+
try {
|
|
114
|
+
const parts = token.split('.');
|
|
115
|
+
if (parts.length !== 3)
|
|
116
|
+
return null;
|
|
117
|
+
const header = JSON.parse(base64UrlDecodeToString(parts[0]));
|
|
118
|
+
return typeof header === 'object' && header !== null ? header : null;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function parseAndValidatePayload(payloadPart) {
|
|
125
|
+
try {
|
|
126
|
+
const payload = JSON.parse(base64UrlDecodeToString(payloadPart));
|
|
127
|
+
if (typeof payload !== 'object' || payload === null)
|
|
128
|
+
return null;
|
|
129
|
+
if (typeof payload.sub !== 'string' || payload.sub.length === 0)
|
|
130
|
+
return null;
|
|
131
|
+
if (typeof payload.email !== 'string' || payload.email.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
if (typeof payload.exp !== 'number' || isNaN(payload.exp))
|
|
134
|
+
return null;
|
|
135
|
+
if (typeof payload.iat !== 'number' || isNaN(payload.iat))
|
|
136
|
+
return null;
|
|
137
|
+
return {
|
|
138
|
+
sub: payload.sub,
|
|
139
|
+
email: payload.email,
|
|
140
|
+
roles: Array.isArray(payload.roles)
|
|
141
|
+
? payload.roles.filter((r) => typeof r === 'string')
|
|
142
|
+
: [],
|
|
143
|
+
exp: payload.exp,
|
|
144
|
+
iat: payload.iat
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a JWKS verifier bound to one JWKS URL. The underlying key cache is
|
|
153
|
+
* shared per isolate across verifiers with the same URL.
|
|
154
|
+
*/
|
|
155
|
+
export function createJwksVerifier(userOptions) {
|
|
156
|
+
const options = {
|
|
157
|
+
jwksUrl: userOptions.jwksUrl,
|
|
158
|
+
fetchFn: userOptions.fetchFn,
|
|
159
|
+
cacheTtlMs: userOptions.cacheTtlMs ?? 600_000,
|
|
160
|
+
refreshMinIntervalMs: userOptions.refreshMinIntervalMs ?? 10_000,
|
|
161
|
+
timeoutMs: userOptions.timeoutMs ?? 5_000,
|
|
162
|
+
logger: userOptions.logger
|
|
163
|
+
};
|
|
164
|
+
const log = options.logger ?? createLogger('sentinel-jwks');
|
|
165
|
+
async function resolveKey(kid) {
|
|
166
|
+
let entry = await getJwksEntry(options, false, log);
|
|
167
|
+
if (entry?.keys.has(kid))
|
|
168
|
+
return { key: entry.keys.get(kid), entry };
|
|
169
|
+
// Unknown kid (or no cache): one throttled forced refresh for key rotation.
|
|
170
|
+
entry = await getJwksEntry(options, true, log);
|
|
171
|
+
if (entry?.keys.has(kid))
|
|
172
|
+
return { key: entry.keys.get(kid), entry };
|
|
173
|
+
return { entry };
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
async verify(token) {
|
|
177
|
+
try {
|
|
178
|
+
const parts = token.split('.');
|
|
179
|
+
if (parts.length !== 3) {
|
|
180
|
+
return { status: 'invalid', reason: 'malformed token structure' };
|
|
181
|
+
}
|
|
182
|
+
const [headerPart, payloadPart, signaturePart] = parts;
|
|
183
|
+
const header = parseTokenHeader(token);
|
|
184
|
+
if (!header) {
|
|
185
|
+
return { status: 'invalid', reason: 'unparseable token header' };
|
|
186
|
+
}
|
|
187
|
+
if (header.alg !== 'EdDSA') {
|
|
188
|
+
return {
|
|
189
|
+
status: 'invalid',
|
|
190
|
+
reason: `unsupported token alg "${String(header.alg)}" — only EdDSA tokens can be JWKS-verified (backend must be migrated from HS256)`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (typeof header.kid !== 'string' || header.kid.length === 0) {
|
|
194
|
+
return { status: 'invalid', reason: 'token header missing kid' };
|
|
195
|
+
}
|
|
196
|
+
const { key, entry } = await resolveKey(header.kid);
|
|
197
|
+
if (!entry) {
|
|
198
|
+
return { status: 'unavailable', reason: 'JWKS document unreachable' };
|
|
199
|
+
}
|
|
200
|
+
if (!key) {
|
|
201
|
+
return { status: 'invalid', reason: `unknown kid "${header.kid}" (after JWKS refresh)` };
|
|
202
|
+
}
|
|
203
|
+
const signature = base64UrlDecodeToBytes(signaturePart);
|
|
204
|
+
const data = new TextEncoder().encode(`${headerPart}.${payloadPart}`);
|
|
205
|
+
const signatureValid = await crypto.subtle.verify('Ed25519', key, signature, data);
|
|
206
|
+
if (!signatureValid) {
|
|
207
|
+
return { status: 'invalid', reason: 'signature verification failed' };
|
|
208
|
+
}
|
|
209
|
+
const payload = parseAndValidatePayload(payloadPart);
|
|
210
|
+
if (!payload) {
|
|
211
|
+
return { status: 'invalid', reason: 'payload missing required claims' };
|
|
212
|
+
}
|
|
213
|
+
if (Date.now() >= payload.exp * 1000) {
|
|
214
|
+
return { status: 'invalid', reason: 'token expired' };
|
|
215
|
+
}
|
|
216
|
+
return { status: 'valid', payload };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Never throw out of verification — classify and fail closed.
|
|
220
|
+
log.error('Unexpected JWKS verification error', { error: String(error) });
|
|
221
|
+
return { status: 'invalid', reason: 'verification error' };
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
_resetCacheForTests() {
|
|
225
|
+
jwksCache.delete(options.jwksUrl);
|
|
226
|
+
lastForcedRefreshAt.delete(options.jwksUrl);
|
|
227
|
+
inflightFetches.delete(options.jwksUrl);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=jwks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks.js","sourceRoot":"","sources":["../../src/core/jwks.ts"],"names":[],"mappings":"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","sourcesContent":["// src/core/jwks.ts\n\n/**\n * JWKS-based access-token signature verification (Ed25519 / EdDSA).\n *\n * Security model: the Sentinel backend signs access tokens with EdDSA and\n * publishes the public key at `/.well-known/jwks.json` (RFC 7517). This\n * module fetches and caches that key set, then verifies each access token's\n * signature with WebCrypto BEFORE the payload is trusted. Tokens that fail\n * verification are treated as unauthenticated — verification never throws.\n *\n * Cache behaviour:\n * - JWKS documents are cached per isolate, keyed by URL, with a TTL\n * (default 10 minutes).\n * - A token whose `kid` is not in the cache triggers ONE forced refresh\n * (key rotation support), throttled to at most once per\n * `refreshMinIntervalMs` (default 10 seconds) so unknown-kid floods cannot\n * be used to hammer the auth server.\n * - JWKS fetch failure classifies as `unavailable` (treat as\n * unauthenticated); it never surfaces as a 500.\n *\n * Only OKP/Ed25519 keys with `alg: EdDSA` semantics are accepted. Tokens\n * with any other algorithm (including legacy HS256) are rejected as\n * `invalid` — a symmetric HS256 secret can never be verified from a public\n * JWKS, and accepting it here would reintroduce the unverified-payload hole.\n */\n\nimport type { DecodedToken } from './types.js';\nimport type { SentinelLogger } from './logger.js';\nimport { createLogger } from './logger.js';\n\nexport interface JwksVerifierOptions {\n\t/** Full URL of the JWKS document, e.g. https://auth.example.com/.well-known/jwks.json */\n\tjwksUrl: string;\n\t/** Fetch implementation. Defaults to globalThis.fetch (pass event.fetch in SvelteKit handles). */\n\tfetchFn?: typeof globalThis.fetch;\n\t/** Cache TTL in milliseconds. Default: 600_000 (10 minutes). */\n\tcacheTtlMs?: number;\n\t/** Minimum interval between forced refreshes for unknown kids, in ms. Default: 10_000. */\n\trefreshMinIntervalMs?: number;\n\t/** Timeout for each JWKS fetch, in ms. Default: 5_000. */\n\ttimeoutMs?: number;\n\t/** Optional structured logger. */\n\tlogger?: SentinelLogger;\n}\n\nexport type TokenVerifyOutcome =\n\t/** Signature valid, payload structurally sound and not expired. */\n\t| { status: 'valid'; payload: DecodedToken }\n\t/**\n\t * Token rejected: bad signature, malformed token, unsupported alg,\n\t * unknown kid (after refresh), expired, or structurally invalid payload.\n\t * Treat the request as unauthenticated.\n\t */\n\t| { status: 'invalid'; reason: string }\n\t/**\n\t * Verification could not run because the JWKS document is unreachable or\n\t * unusable. Treat the request as unauthenticated (fail closed) — never a 500.\n\t */\n\t| { status: 'unavailable'; reason: string };\n\nexport interface JwksVerifier {\n\tverify(token: string): Promise<TokenVerifyOutcome>;\n\t/** Test-only: drop cached JWKS state for this verifier's URL. */\n\t_resetCacheForTests(): void;\n}\n\ninterface JwksCacheEntry {\n\tkeys: Map<string, CryptoKey>;\n\tfetchedAt: number;\n}\n\ninterface JwksKeyShape {\n\tkty?: unknown;\n\tcrv?: unknown;\n\tx?: unknown;\n\tkid?: unknown;\n\talg?: unknown;\n\tuse?: unknown;\n}\n\ninterface JwksDocumentShape {\n\tkeys?: unknown;\n}\n\n// Per-isolate cache shared across verifiers with the same JWKS URL.\nconst jwksCache = new Map<string, JwksCacheEntry>();\n// Per-URL in-flight fetch dedup (avoids thundering herd on cold cache).\nconst inflightFetches = new Map<string, Promise<JwksCacheEntry | null>>();\n// Per-URL throttle markers for forced refreshes on unknown kid.\nconst lastForcedRefreshAt = new Map<string, number>();\n\n/** Test-only: drop all cached JWKS state. */\nexport function __resetJwksCacheForTests(): void {\n\tjwksCache.clear();\n\tinflightFetches.clear();\n\tlastForcedRefreshAt.clear();\n}\n\nfunction base64UrlDecodeToBytes(input: string): Uint8Array<ArrayBuffer> {\n\tlet base64 = input.replace(/-/g, '+').replace(/_/g, '/');\n\tconst pad = base64.length % 4;\n\tif (pad) base64 += '='.repeat(4 - pad);\n\tconst binary = atob(base64);\n\tconst bytes = new Uint8Array(new ArrayBuffer(binary.length));\n\tfor (let i = 0; i < binary.length; i += 1) {\n\t\tbytes[i] = binary.charCodeAt(i);\n\t}\n\treturn bytes;\n}\n\nfunction base64UrlDecodeToString(input: string): string {\n\t// Decode via Uint8Array + TextDecoder for proper UTF-8 handling.\n\treturn new TextDecoder('utf-8').decode(base64UrlDecodeToBytes(input));\n}\n\nasync function importPublicEd25519Key(jwk: JwksKeyShape): Promise<CryptoKey | null> {\n\tif (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string' || jwk.x.length === 0) {\n\t\treturn null;\n\t}\n\ttry {\n\t\treturn await crypto.subtle.importKey(\n\t\t\t'jwk',\n\t\t\t{ kty: 'OKP', crv: 'Ed25519', x: jwk.x },\n\t\t\t{ name: 'Ed25519' },\n\t\t\tfalse,\n\t\t\t['verify']\n\t\t);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nasync function fetchJwksDocument(\n\tjwksUrl: string,\n\tfetchFn: typeof globalThis.fetch,\n\ttimeoutMs: number,\n\tlog: SentinelLogger\n): Promise<JwksCacheEntry | null> {\n\tconst controller = new AbortController();\n\tconst timer = setTimeout(() => controller.abort(), timeoutMs);\n\ttry {\n\t\tconst response = await fetchFn(jwksUrl, {\n\t\t\tsignal: controller.signal,\n\t\t\theaders: { accept: 'application/json' }\n\t\t});\n\t\tif (!response.ok) {\n\t\t\tlog.warn('JWKS fetch returned non-OK status', { jwksUrl, status: response.status });\n\t\t\treturn null;\n\t\t}\n\t\tconst document = (await response.json()) as JwksDocumentShape;\n\t\tif (!Array.isArray(document.keys)) {\n\t\t\tlog.warn('JWKS document has no keys array', { jwksUrl });\n\t\t\treturn null;\n\t\t}\n\t\tconst keys = new Map<string, CryptoKey>();\n\t\tfor (const rawKey of document.keys as JwksKeyShape[]) {\n\t\t\tif (typeof rawKey?.kid !== 'string' || rawKey.kid.length === 0) continue;\n\t\t\tconst imported = await importPublicEd25519Key(rawKey);\n\t\t\tif (imported) keys.set(rawKey.kid, imported);\n\t\t}\n\t\tif (keys.size === 0) {\n\t\t\tlog.warn('JWKS document contained no usable Ed25519 keys', { jwksUrl });\n\t\t\treturn null;\n\t\t}\n\t\treturn { keys, fetchedAt: Date.now() };\n\t} catch (error) {\n\t\tlog.warn('JWKS fetch failed', { jwksUrl, error: String(error) });\n\t\treturn null;\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\nasync function getJwksEntry(\n\toptions: Required<Pick<JwksVerifierOptions, 'jwksUrl' | 'cacheTtlMs' | 'refreshMinIntervalMs' | 'timeoutMs'>> & JwksVerifierOptions,\n\tforce: boolean,\n\tlog: SentinelLogger\n): Promise<JwksCacheEntry | null> {\n\tconst { jwksUrl, cacheTtlMs, refreshMinIntervalMs, timeoutMs } = options;\n\tconst fetchFn = options.fetchFn ?? globalThis.fetch;\n\tconst now = Date.now();\n\n\tconst cached = jwksCache.get(jwksUrl);\n\tif (!force && cached && now - cached.fetchedAt < cacheTtlMs) {\n\t\treturn cached;\n\t}\n\n\tif (force) {\n\t\tconst lastForced = lastForcedRefreshAt.get(jwksUrl) ?? 0;\n\t\tif (now - lastForced < refreshMinIntervalMs) {\n\t\t\t// Throttled: report whatever we have (possibly nothing).\n\t\t\treturn cached ?? null;\n\t\t}\n\t\tlastForcedRefreshAt.set(jwksUrl, now);\n\t}\n\n\tconst inflight = inflightFetches.get(jwksUrl);\n\tif (inflight) return inflight;\n\n\tconst pending = fetchJwksDocument(jwksUrl, fetchFn, timeoutMs, log)\n\t\t.then((entry) => {\n\t\t\tif (entry) jwksCache.set(jwksUrl, entry);\n\t\t\treturn entry;\n\t\t})\n\t\t.finally(() => {\n\t\t\tinflightFetches.delete(jwksUrl);\n\t\t});\n\tinflightFetches.set(jwksUrl, pending);\n\treturn pending;\n}\n\nfunction parseTokenHeader(token: string): { alg?: unknown; kid?: unknown } | null {\n\ttry {\n\t\tconst parts = token.split('.');\n\t\tif (parts.length !== 3) return null;\n\t\tconst header = JSON.parse(base64UrlDecodeToString(parts[0]));\n\t\treturn typeof header === 'object' && header !== null ? (header as { alg?: unknown; kid?: unknown }) : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction parseAndValidatePayload(payloadPart: string): DecodedToken | null {\n\ttry {\n\t\tconst payload = JSON.parse(base64UrlDecodeToString(payloadPart));\n\t\tif (typeof payload !== 'object' || payload === null) return null;\n\t\tif (typeof payload.sub !== 'string' || payload.sub.length === 0) return null;\n\t\tif (typeof payload.email !== 'string' || payload.email.length === 0) return null;\n\t\tif (typeof payload.exp !== 'number' || isNaN(payload.exp)) return null;\n\t\tif (typeof payload.iat !== 'number' || isNaN(payload.iat)) return null;\n\t\treturn {\n\t\t\tsub: payload.sub,\n\t\t\temail: payload.email,\n\t\t\troles: Array.isArray(payload.roles)\n\t\t\t\t? payload.roles.filter((r: unknown): r is string => typeof r === 'string')\n\t\t\t\t: [],\n\t\t\texp: payload.exp,\n\t\t\tiat: payload.iat\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Create a JWKS verifier bound to one JWKS URL. The underlying key cache is\n * shared per isolate across verifiers with the same URL.\n */\nexport function createJwksVerifier(userOptions: JwksVerifierOptions): JwksVerifier {\n\tconst options = {\n\t\tjwksUrl: userOptions.jwksUrl,\n\t\tfetchFn: userOptions.fetchFn,\n\t\tcacheTtlMs: userOptions.cacheTtlMs ?? 600_000,\n\t\trefreshMinIntervalMs: userOptions.refreshMinIntervalMs ?? 10_000,\n\t\ttimeoutMs: userOptions.timeoutMs ?? 5_000,\n\t\tlogger: userOptions.logger\n\t};\n\tconst log = options.logger ?? createLogger('sentinel-jwks');\n\n\tasync function resolveKey(kid: string): Promise<{ key?: CryptoKey; entry: JwksCacheEntry | null }> {\n\t\tlet entry = await getJwksEntry(options, false, log);\n\t\tif (entry?.keys.has(kid)) return { key: entry.keys.get(kid), entry };\n\n\t\t// Unknown kid (or no cache): one throttled forced refresh for key rotation.\n\t\tentry = await getJwksEntry(options, true, log);\n\t\tif (entry?.keys.has(kid)) return { key: entry.keys.get(kid), entry };\n\t\treturn { entry };\n\t}\n\n\treturn {\n\t\tasync verify(token: string): Promise<TokenVerifyOutcome> {\n\t\t\ttry {\n\t\t\t\tconst parts = token.split('.');\n\t\t\t\tif (parts.length !== 3) {\n\t\t\t\t\treturn { status: 'invalid', reason: 'malformed token structure' };\n\t\t\t\t}\n\t\t\t\tconst [headerPart, payloadPart, signaturePart] = parts;\n\n\t\t\t\tconst header = parseTokenHeader(token);\n\t\t\t\tif (!header) {\n\t\t\t\t\treturn { status: 'invalid', reason: 'unparseable token header' };\n\t\t\t\t}\n\t\t\t\tif (header.alg !== 'EdDSA') {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: 'invalid',\n\t\t\t\t\t\treason: `unsupported token alg \"${String(header.alg)}\" — only EdDSA tokens can be JWKS-verified (backend must be migrated from HS256)`\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tif (typeof header.kid !== 'string' || header.kid.length === 0) {\n\t\t\t\t\treturn { status: 'invalid', reason: 'token header missing kid' };\n\t\t\t\t}\n\n\t\t\t\tconst { key, entry } = await resolveKey(header.kid);\n\t\t\t\tif (!entry) {\n\t\t\t\t\treturn { status: 'unavailable', reason: 'JWKS document unreachable' };\n\t\t\t\t}\n\t\t\t\tif (!key) {\n\t\t\t\t\treturn { status: 'invalid', reason: `unknown kid \"${header.kid}\" (after JWKS refresh)` };\n\t\t\t\t}\n\n\t\t\t\tconst signature = base64UrlDecodeToBytes(signaturePart);\n\t\t\t\tconst data = new TextEncoder().encode(`${headerPart}.${payloadPart}`);\n\t\t\t\tconst signatureValid = await crypto.subtle.verify('Ed25519', key, signature, data);\n\t\t\t\tif (!signatureValid) {\n\t\t\t\t\treturn { status: 'invalid', reason: 'signature verification failed' };\n\t\t\t\t}\n\n\t\t\t\tconst payload = parseAndValidatePayload(payloadPart);\n\t\t\t\tif (!payload) {\n\t\t\t\t\treturn { status: 'invalid', reason: 'payload missing required claims' };\n\t\t\t\t}\n\t\t\t\tif (Date.now() >= payload.exp * 1000) {\n\t\t\t\t\treturn { status: 'invalid', reason: 'token expired' };\n\t\t\t\t}\n\t\t\t\treturn { status: 'valid', payload };\n\t\t\t} catch (error) {\n\t\t\t\t// Never throw out of verification — classify and fail closed.\n\t\t\t\tlog.error('Unexpected JWKS verification error', { error: String(error) });\n\t\t\t\treturn { status: 'invalid', reason: 'verification error' };\n\t\t\t}\n\t\t},\n\n\t\t_resetCacheForTests(): void {\n\t\t\tjwksCache.delete(options.jwksUrl);\n\t\t\tlastForcedRefreshAt.delete(options.jwksUrl);\n\t\t\tinflightFetches.delete(options.jwksUrl);\n\t\t}\n\t};\n}\n"]}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -135,6 +135,21 @@ export interface SentinelHandleOptions {
|
|
|
135
135
|
baseUrl?: string;
|
|
136
136
|
/** Optional structured logger for auth middleware. */
|
|
137
137
|
logger?: SentinelLogger;
|
|
138
|
+
/**
|
|
139
|
+
* Full JWKS URL used to verify access-token signatures.
|
|
140
|
+
* Default: derived from `apiUrl` (origin + `/.well-known/jwks.json`).
|
|
141
|
+
* Also overridable via the `SENTINEL_JWKS_URL` environment variable.
|
|
142
|
+
*/
|
|
143
|
+
jwksUrl?: string;
|
|
144
|
+
/** JWKS cache TTL in milliseconds. Default: 600_000 (10 minutes). */
|
|
145
|
+
jwksCacheTtlMs?: number;
|
|
146
|
+
/**
|
|
147
|
+
* DEVELOPMENT ONLY: disable JWKS signature verification and fall back to
|
|
148
|
+
* decoding the (unverified) token payload locally. Equivalent to setting
|
|
149
|
+
* the environment variable `SENTINEL_SKIP_JWKS_VERIFY=1`. Never enable
|
|
150
|
+
* this in production — the admin gate would again trust unverified claims.
|
|
151
|
+
*/
|
|
152
|
+
skipJwksVerify?: boolean;
|
|
138
153
|
}
|
|
139
154
|
/** Data passed to the onBeforeLogin hook. */
|
|
140
155
|
export interface BeforeLoginContext {
|
package/dist/core/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
|
package/dist/core/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"","sourcesContent":["// src/core/types.ts\nimport type { SentinelLogger } from './logger.js';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nexport interface SentinelConfig {\n\tapiUrl: string;\n\tbaseUrl: string;\n\t/**\n\t * When true, validateToken() performs a second GET /users/me request after\n\t * POST /auth/validate to refresh DB-backed profile fields such as roles.\n\t * Default: false. Keep disabled for high-volume per-request validation paths.\n\t */\n\thydrateUserProfile?: boolean;\n\t/**\n\t * Timeout in milliseconds for each individual fetch call to the Sentinel API.\n\t * Default: 8000 (8 seconds).\n\t */\n\ttimeoutMs?: number;\n\t/**\n\t * Maximum number of retries for retryable errors (network timeout, HTTP 502/503/504).\n\t * Non-retryable errors (400, 401, 403) are never retried.\n\t * Default: 1.\n\t */\n\tmaxRetries?: number;\n\t/** Optional structured logger. Falls back to console if not provided. */\n\tlogger?: SentinelLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Typed API Responses (replaces the old untyped SentinelResponse bag)\n// ---------------------------------------------------------------------------\n\n/** Base fields shared by all Sentinel API responses. */\nexport interface SentinelResponseBase {\n\tsuccess?: boolean;\n\tcode?: number;\n\tmessage?: string;\n\terror?: string;\n}\n\n/** Response from POST /auth/login-passwordless */\nexport interface StartLoginResponse extends SentinelResponseBase {\n\ttransactionId?: string;\n\tclientToken?: string;\n}\n\n/** Response from POST /auth/poll */\nexport interface PollResponse extends SentinelResponseBase {\n\tstatus?: 'pending' | 'completed' | 'expired';\n\t/**\n\t * Opaque transaction-bound redirect hint echoed back on the claimed poll\n\t * response. Present only when the originating register/startLogin call\n\t * supplied a `redirectContext`. Null or absent otherwise.\n\t */\n\tredirect_context?: string | null;\n}\n\n/** Response from POST /auth/claim */\nexport interface ClaimResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n}\n\n/** Response from POST /auth/refresh */\nexport interface RefreshResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n}\n\n/** Response from POST /auth/validate */\nexport interface ValidateResponse extends SentinelResponseBase {\n\tvalid?: boolean;\n\tpayload?: DecodedToken;\n\tuser?: {\n\t\tsub: string;\n\t\temail: string;\n\t\troles: string[];\n\t};\n}\n\n/** Response from GET /users/me. */\nexport interface CurrentUserResponse extends SentinelResponseBase {\n\tid?: string;\n\temail?: string;\n\trole?: string;\n\troles?: string[];\n\tstatus?: string;\n\tlegalName?: string;\n\tocmeMemberNumber?: string;\n\twalletAddress?: string;\n\taliasName?: string;\n\ttwitterAccount?: string;\n\tyoutubeAccount?: string;\n\tcubicubeAddress?: string;\n}\n\n/** Response from POST /auth/register */\nexport interface RegisterResponse extends SentinelResponseBase {}\n\n/** Response from POST /auth/verify */\nexport interface VerifyResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n\t/**\n\t * Opaque transaction-bound redirect hint echoed back from the originating\n\t * register call. Present only when a `redirectContext` was supplied. Null\n\t * or absent otherwise.\n\t */\n\tredirect_context?: string | null;\n}\n\n/** Response from POST /auth/complete-passwordless */\nexport interface CompletePasswordlessResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n\t/**\n\t * Opaque transaction-bound redirect hint echoed back from the originating\n\t * startLogin call. Present only when a `redirectContext` was supplied. Null\n\t * or absent otherwise.\n\t */\n\tredirect_context?: string | null;\n}\n\n/**\n * @deprecated Use the specific typed response interfaces instead.\n * Kept for backward compatibility — will be removed in v2.0.0.\n */\nexport interface SentinelResponse {\n\tsuccess?: boolean;\n\tcode?: number;\n\tmessage?: string;\n\terror?: string;\n\t[key: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// JWT / Auth\n// ---------------------------------------------------------------------------\n\nexport interface DecodedToken {\n\tsub: string;\n\temail: string;\n\troles: string[];\n\texp: number;\n\tiat: number;\n}\n\nexport interface UserLocals {\n\tisAuthenticated: boolean;\n\tid: string;\n\temail: string;\n\troles: string[];\n}\n\n// ---------------------------------------------------------------------------\n// SvelteKit Handle Options\n// ---------------------------------------------------------------------------\n\nexport interface SentinelHandleOptions {\n\tprotectedRoutes?: string[];\n\tloginRedirect?: string;\n\tapiUrl?: string;\n\tbaseUrl?: string;\n\t/** Optional structured logger for auth middleware. */\n\tlogger?: SentinelLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Auth Handler Options & Lifecycle Hooks\n// ---------------------------------------------------------------------------\n\n/** Data passed to the onBeforeLogin hook. */\nexport interface BeforeLoginContext {\n\temail: string;\n\trememberMe: boolean;\n\tredirectTo?: string;\n}\n\n/**\n * Return this from onBeforeLogin to reject the request.\n *\n * When `redirect` is set and is a safe relative path (starts with `/`, no\n * protocol, no `//` / `\\\\` prefix), the SDK issues a `303 See Other` to that\n * path instead of returning the JSON error body. Unsafe redirects are ignored\n * and the JSON-reject path is used as a fallback.\n *\n * @example\n * ```ts\n * onBeforeLogin: async (_event, { email }) => {\n * const gate = await isLoginAllowed(email);\n * if (!gate.ok) {\n * return {\n * error: gate.reason,\n * status: 403,\n * redirect: gate.reason === 'waitlisted' ? '/waitlist' : '/unauthorized'\n * };\n * }\n * }\n * ```\n */\nexport interface BeforeLoginReject {\n\terror: string;\n\tstatus?: number;\n\t/**\n\t * Optional branded destination the SDK will 303-redirect to instead of\n\t * returning a JSON error. Must be a safe relative path (starts with `/`,\n\t * no protocol, no `//` / `\\\\` prefix). Unsafe values fall back to the\n\t * JSON-reject behaviour.\n\t */\n\tredirect?: string;\n}\n\n/** Data passed to the onAfterLogin hook. */\nexport interface AfterLoginContext {\n\temail: string;\n\ttransactionId: string;\n}\n\n/** Data passed to the onAfterClaim hook. */\nexport interface AfterClaimContext {\n\taccessToken: string;\n\trefreshToken: string;\n\tredirectTo?: string;\n}\n\n/** Data passed to the onError hook. */\nexport interface ErrorContext {\n\thandler: 'start-login' | 'poll' | 'claim' | 'logout' | 'refresh' | 'register' | 'verify';\n\terror: unknown;\n}\n\n/**\n * Context passed to the `resolveRedirect` hook after a successful token claim.\n * Hosts use this to pick a post-login destination based on the user's roles,\n * a requested redirect, or fall back to the configured default.\n */\nexport interface ResolveRedirectContext {\n\t/** Newly authenticated user, decoded from the access token. */\n\tuser: UserLocals;\n\t/** Access token just issued for this user. */\n\taccessToken: string;\n\t/** Whether the user opted into a longer session at login time. */\n\trememberMe: boolean;\n\t/**\n\t * The redirect the client originally requested (from the `login_redirect_to`\n\t * cookie, `?redirect=` on `/auth`, or the `redirectTo` body field).\n\t * Already sanitised to a safe relative path by the SDK.\n\t */\n\trequestedRedirect?: string;\n\t/** The redirect that would be used if the resolver is absent. Always safe. */\n\tdefaultRedirect: string;\n}\n\n/**\n * Context passed to the `logoutRedirect` function form, if the host wants to\n * decide the post-logout landing at request time (e.g. send authenticated\n * users to `/` and anonymous users to `/auth`).\n */\nexport interface LogoutRedirectContext {\n\t/** User present when logout was invoked, if any (may be null for stale sessions). */\n\tuser: UserLocals | null;\n}\n\nexport interface AuthHandlerOptions {\n\t/**\n\t * Login cookie TTL in seconds. Controls how long the login transaction\n\t * cookies (transaction ID, client token, remember-me) persist.\n\t * Default: 300 (5 minutes).\n\t */\n\tloginCookieTtlSeconds?: number;\n\n\t/**\n\t * Path to redirect unauthenticated users (logout, claim errors).\n\t * Default: '/auth'.\n\t */\n\tloginRedirect?: string;\n\n\t/**\n\t * Store a `login_redirect_to` cookie from the request body's `redirectTo`\n\t * field, then use it on claim to redirect the user back to their original page.\n\t * Default: true.\n\t */\n\tredirectCookie?: boolean;\n\n\t/** Optional structured logger for auth handlers. */\n\tlogger?: SentinelLogger;\n\n\t/**\n\t * Runs before the startLogin API call.\n\t * Return `{ error, status? }` to reject the request.\n\t */\n\tonBeforeLogin?: (event: import('@sveltejs/kit').RequestEvent, ctx: BeforeLoginContext) => Promise<BeforeLoginReject | void> | BeforeLoginReject | void;\n\n\t/** Runs after a successful startLogin (transaction cookies set). */\n\tonAfterLogin?: (event: import('@sveltejs/kit').RequestEvent, ctx: AfterLoginContext) => Promise<void> | void;\n\n\t/** Runs after successful token claim (session cookies set). */\n\tonAfterClaim?: (event: import('@sveltejs/kit').RequestEvent, ctx: AfterClaimContext) => Promise<void> | void;\n\n\t/** Runs on any handler error. */\n\tonError?: (event: import('@sveltejs/kit').RequestEvent, ctx: ErrorContext) => Promise<void> | void;\n\n\t/**\n\t * Called after a successful token claim to decide the post-login destination.\n\t * Return a safe relative path (starts with `/`, no protocol, no `//` prefix).\n\t * If the resolver throws, returns a non-string, an empty string, or an unsafe\n\t * path, the SDK falls back to `ctx.defaultRedirect`.\n\t *\n\t * @example\n\t * ```ts\n\t * resolveRedirect: async ({ user, requestedRedirect, defaultRedirect }) => {\n\t * if (user.roles.includes('admin')) return '/admin';\n\t * return requestedRedirect || defaultRedirect;\n\t * }\n\t * ```\n\t */\n\tresolveRedirect?: (\n\t\tevent: import('@sveltejs/kit').RequestEvent,\n\t\tctx: ResolveRedirectContext\n\t) => Promise<string> | string;\n\n\t/**\n\t * Where `POST /logout` and `GET /logout` redirect after clearing cookies.\n\t *\n\t * - Omitted: falls back to `loginRedirect` (current behaviour, `/auth` by default).\n\t * - String: treated as a literal safe relative path. Unsafe values fall back to `loginRedirect`.\n\t * - Function: called with the pre-logout user context; return a safe relative\n\t * path (async permitted). Throws / empty / unsafe → falls back to `loginRedirect`.\n\t *\n\t * @example\n\t * ```ts\n\t * logoutRedirect: '/'\n\t * // or\n\t * logoutRedirect: ({ user }) => user ? '/' : '/auth'\n\t * ```\n\t */\n\tlogoutRedirect?:\n\t\t| string\n\t\t| ((\n\t\t\t\tevent: import('@sveltejs/kit').RequestEvent,\n\t\t\t\tctx: LogoutRedirectContext\n\t\t ) => Promise<string> | string);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"","sourcesContent":["// src/core/types.ts\nimport type { SentinelLogger } from './logger.js';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nexport interface SentinelConfig {\n\tapiUrl: string;\n\tbaseUrl: string;\n\t/**\n\t * When true, validateToken() performs a second GET /users/me request after\n\t * POST /auth/validate to refresh DB-backed profile fields such as roles.\n\t * Default: false. Keep disabled for high-volume per-request validation paths.\n\t */\n\thydrateUserProfile?: boolean;\n\t/**\n\t * Timeout in milliseconds for each individual fetch call to the Sentinel API.\n\t * Default: 8000 (8 seconds).\n\t */\n\ttimeoutMs?: number;\n\t/**\n\t * Maximum number of retries for retryable errors (network timeout, HTTP 502/503/504).\n\t * Non-retryable errors (400, 401, 403) are never retried.\n\t * Default: 1.\n\t */\n\tmaxRetries?: number;\n\t/** Optional structured logger. Falls back to console if not provided. */\n\tlogger?: SentinelLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Typed API Responses (replaces the old untyped SentinelResponse bag)\n// ---------------------------------------------------------------------------\n\n/** Base fields shared by all Sentinel API responses. */\nexport interface SentinelResponseBase {\n\tsuccess?: boolean;\n\tcode?: number;\n\tmessage?: string;\n\terror?: string;\n}\n\n/** Response from POST /auth/login-passwordless */\nexport interface StartLoginResponse extends SentinelResponseBase {\n\ttransactionId?: string;\n\tclientToken?: string;\n}\n\n/** Response from POST /auth/poll */\nexport interface PollResponse extends SentinelResponseBase {\n\tstatus?: 'pending' | 'completed' | 'expired';\n\t/**\n\t * Opaque transaction-bound redirect hint echoed back on the claimed poll\n\t * response. Present only when the originating register/startLogin call\n\t * supplied a `redirectContext`. Null or absent otherwise.\n\t */\n\tredirect_context?: string | null;\n}\n\n/** Response from POST /auth/claim */\nexport interface ClaimResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n}\n\n/** Response from POST /auth/refresh */\nexport interface RefreshResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n}\n\n/** Response from POST /auth/validate */\nexport interface ValidateResponse extends SentinelResponseBase {\n\tvalid?: boolean;\n\tpayload?: DecodedToken;\n\tuser?: {\n\t\tsub: string;\n\t\temail: string;\n\t\troles: string[];\n\t};\n}\n\n/** Response from GET /users/me. */\nexport interface CurrentUserResponse extends SentinelResponseBase {\n\tid?: string;\n\temail?: string;\n\trole?: string;\n\troles?: string[];\n\tstatus?: string;\n\tlegalName?: string;\n\tocmeMemberNumber?: string;\n\twalletAddress?: string;\n\taliasName?: string;\n\ttwitterAccount?: string;\n\tyoutubeAccount?: string;\n\tcubicubeAddress?: string;\n}\n\n/** Response from POST /auth/register */\nexport interface RegisterResponse extends SentinelResponseBase {}\n\n/** Response from POST /auth/verify */\nexport interface VerifyResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n\t/**\n\t * Opaque transaction-bound redirect hint echoed back from the originating\n\t * register call. Present only when a `redirectContext` was supplied. Null\n\t * or absent otherwise.\n\t */\n\tredirect_context?: string | null;\n}\n\n/** Response from POST /auth/complete-passwordless */\nexport interface CompletePasswordlessResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n\t/**\n\t * Opaque transaction-bound redirect hint echoed back from the originating\n\t * startLogin call. Present only when a `redirectContext` was supplied. Null\n\t * or absent otherwise.\n\t */\n\tredirect_context?: string | null;\n}\n\n/**\n * @deprecated Use the specific typed response interfaces instead.\n * Kept for backward compatibility — will be removed in v2.0.0.\n */\nexport interface SentinelResponse {\n\tsuccess?: boolean;\n\tcode?: number;\n\tmessage?: string;\n\terror?: string;\n\t[key: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// JWT / Auth\n// ---------------------------------------------------------------------------\n\nexport interface DecodedToken {\n\tsub: string;\n\temail: string;\n\troles: string[];\n\texp: number;\n\tiat: number;\n}\n\nexport interface UserLocals {\n\tisAuthenticated: boolean;\n\tid: string;\n\temail: string;\n\troles: string[];\n}\n\n// ---------------------------------------------------------------------------\n// SvelteKit Handle Options\n// ---------------------------------------------------------------------------\n\nexport interface SentinelHandleOptions {\n\tprotectedRoutes?: string[];\n\tloginRedirect?: string;\n\tapiUrl?: string;\n\tbaseUrl?: string;\n\t/** Optional structured logger for auth middleware. */\n\tlogger?: SentinelLogger;\n\t/**\n\t * Full JWKS URL used to verify access-token signatures.\n\t * Default: derived from `apiUrl` (origin + `/.well-known/jwks.json`).\n\t * Also overridable via the `SENTINEL_JWKS_URL` environment variable.\n\t */\n\tjwksUrl?: string;\n\t/** JWKS cache TTL in milliseconds. Default: 600_000 (10 minutes). */\n\tjwksCacheTtlMs?: number;\n\t/**\n\t * DEVELOPMENT ONLY: disable JWKS signature verification and fall back to\n\t * decoding the (unverified) token payload locally. Equivalent to setting\n\t * the environment variable `SENTINEL_SKIP_JWKS_VERIFY=1`. Never enable\n\t * this in production — the admin gate would again trust unverified claims.\n\t */\n\tskipJwksVerify?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Auth Handler Options & Lifecycle Hooks\n// ---------------------------------------------------------------------------\n\n/** Data passed to the onBeforeLogin hook. */\nexport interface BeforeLoginContext {\n\temail: string;\n\trememberMe: boolean;\n\tredirectTo?: string;\n}\n\n/**\n * Return this from onBeforeLogin to reject the request.\n *\n * When `redirect` is set and is a safe relative path (starts with `/`, no\n * protocol, no `//` / `\\\\` prefix), the SDK issues a `303 See Other` to that\n * path instead of returning the JSON error body. Unsafe redirects are ignored\n * and the JSON-reject path is used as a fallback.\n *\n * @example\n * ```ts\n * onBeforeLogin: async (_event, { email }) => {\n * const gate = await isLoginAllowed(email);\n * if (!gate.ok) {\n * return {\n * error: gate.reason,\n * status: 403,\n * redirect: gate.reason === 'waitlisted' ? '/waitlist' : '/unauthorized'\n * };\n * }\n * }\n * ```\n */\nexport interface BeforeLoginReject {\n\terror: string;\n\tstatus?: number;\n\t/**\n\t * Optional branded destination the SDK will 303-redirect to instead of\n\t * returning a JSON error. Must be a safe relative path (starts with `/`,\n\t * no protocol, no `//` / `\\\\` prefix). Unsafe values fall back to the\n\t * JSON-reject behaviour.\n\t */\n\tredirect?: string;\n}\n\n/** Data passed to the onAfterLogin hook. */\nexport interface AfterLoginContext {\n\temail: string;\n\ttransactionId: string;\n}\n\n/** Data passed to the onAfterClaim hook. */\nexport interface AfterClaimContext {\n\taccessToken: string;\n\trefreshToken: string;\n\tredirectTo?: string;\n}\n\n/** Data passed to the onError hook. */\nexport interface ErrorContext {\n\thandler: 'start-login' | 'poll' | 'claim' | 'logout' | 'refresh' | 'register' | 'verify';\n\terror: unknown;\n}\n\n/**\n * Context passed to the `resolveRedirect` hook after a successful token claim.\n * Hosts use this to pick a post-login destination based on the user's roles,\n * a requested redirect, or fall back to the configured default.\n */\nexport interface ResolveRedirectContext {\n\t/** Newly authenticated user, decoded from the access token. */\n\tuser: UserLocals;\n\t/** Access token just issued for this user. */\n\taccessToken: string;\n\t/** Whether the user opted into a longer session at login time. */\n\trememberMe: boolean;\n\t/**\n\t * The redirect the client originally requested (from the `login_redirect_to`\n\t * cookie, `?redirect=` on `/auth`, or the `redirectTo` body field).\n\t * Already sanitised to a safe relative path by the SDK.\n\t */\n\trequestedRedirect?: string;\n\t/** The redirect that would be used if the resolver is absent. Always safe. */\n\tdefaultRedirect: string;\n}\n\n/**\n * Context passed to the `logoutRedirect` function form, if the host wants to\n * decide the post-logout landing at request time (e.g. send authenticated\n * users to `/` and anonymous users to `/auth`).\n */\nexport interface LogoutRedirectContext {\n\t/** User present when logout was invoked, if any (may be null for stale sessions). */\n\tuser: UserLocals | null;\n}\n\nexport interface AuthHandlerOptions {\n\t/**\n\t * Login cookie TTL in seconds. Controls how long the login transaction\n\t * cookies (transaction ID, client token, remember-me) persist.\n\t * Default: 300 (5 minutes).\n\t */\n\tloginCookieTtlSeconds?: number;\n\n\t/**\n\t * Path to redirect unauthenticated users (logout, claim errors).\n\t * Default: '/auth'.\n\t */\n\tloginRedirect?: string;\n\n\t/**\n\t * Store a `login_redirect_to` cookie from the request body's `redirectTo`\n\t * field, then use it on claim to redirect the user back to their original page.\n\t * Default: true.\n\t */\n\tredirectCookie?: boolean;\n\n\t/** Optional structured logger for auth handlers. */\n\tlogger?: SentinelLogger;\n\n\t/**\n\t * Runs before the startLogin API call.\n\t * Return `{ error, status? }` to reject the request.\n\t */\n\tonBeforeLogin?: (event: import('@sveltejs/kit').RequestEvent, ctx: BeforeLoginContext) => Promise<BeforeLoginReject | void> | BeforeLoginReject | void;\n\n\t/** Runs after a successful startLogin (transaction cookies set). */\n\tonAfterLogin?: (event: import('@sveltejs/kit').RequestEvent, ctx: AfterLoginContext) => Promise<void> | void;\n\n\t/** Runs after successful token claim (session cookies set). */\n\tonAfterClaim?: (event: import('@sveltejs/kit').RequestEvent, ctx: AfterClaimContext) => Promise<void> | void;\n\n\t/** Runs on any handler error. */\n\tonError?: (event: import('@sveltejs/kit').RequestEvent, ctx: ErrorContext) => Promise<void> | void;\n\n\t/**\n\t * Called after a successful token claim to decide the post-login destination.\n\t * Return a safe relative path (starts with `/`, no protocol, no `//` prefix).\n\t * If the resolver throws, returns a non-string, an empty string, or an unsafe\n\t * path, the SDK falls back to `ctx.defaultRedirect`.\n\t *\n\t * @example\n\t * ```ts\n\t * resolveRedirect: async ({ user, requestedRedirect, defaultRedirect }) => {\n\t * if (user.roles.includes('admin')) return '/admin';\n\t * return requestedRedirect || defaultRedirect;\n\t * }\n\t * ```\n\t */\n\tresolveRedirect?: (\n\t\tevent: import('@sveltejs/kit').RequestEvent,\n\t\tctx: ResolveRedirectContext\n\t) => Promise<string> | string;\n\n\t/**\n\t * Where `POST /logout` and `GET /logout` redirect after clearing cookies.\n\t *\n\t * - Omitted: falls back to `loginRedirect` (current behaviour, `/auth` by default).\n\t * - String: treated as a literal safe relative path. Unsafe values fall back to `loginRedirect`.\n\t * - Function: called with the pre-logout user context; return a safe relative\n\t * path (async permitted). Throws / empty / unsafe → falls back to `loginRedirect`.\n\t *\n\t * @example\n\t * ```ts\n\t * logoutRedirect: '/'\n\t * // or\n\t * logoutRedirect: ({ user }) => user ? '/' : '/auth'\n\t * ```\n\t */\n\tlogoutRedirect?:\n\t\t| string\n\t\t| ((\n\t\t\t\tevent: import('@sveltejs/kit').RequestEvent,\n\t\t\t\tctx: LogoutRedirectContext\n\t\t ) => Promise<string> | string);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sveltekit/client/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sveltekit/client/index.ts"],"names":[],"mappings":"AAwEA;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAgCD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC9B,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAqCnB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,GAAG,EAC1C,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,CAAC,CAAC,CAQZ"}
|