@nexartis/sentinel-sdk 1.9.1 → 1.9.2

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 CHANGED
@@ -51,6 +51,19 @@ downstream authorization gates.
51
51
  refresh to pick up rotated keys.
52
52
  - Only `alg: EdDSA` is accepted. `HS256`, `none`, and any other algorithm are
53
53
  rejected outright.
54
+ - Since **1.9.2**, signed claims must match the backend's current exact
55
+ `iss: "nexartis"`, `aud: "nexartis"` tuple. Subject, email and JTI must be
56
+ nonempty strings; roles must be an array of strings. `iat`, `exp`, and
57
+ optional `nbf` must be finite nonnegative seconds (including fractional
58
+ seconds) with bounded millisecond conversion. Expired tokens and tokens before `nbf` are rejected.
59
+ - Valid verifier outcomes return only the qualified issuer, audience, subject,
60
+ email, roles, JTI and NumericDate claims. Unqualified additional claims do not
61
+ acquire tenant, client, membership, or human-presence semantics.
62
+ `DecodedToken` and `decodeTokenLocally` alone are **not** verification evidence.
63
+ - Local JWKS verification **does not query backend JTI revocation**, current
64
+ account status or membership, product/tenant authorization, or proof of humanity.
65
+ The current audience is a service-wide constant, not client-specific isolation.
66
+ Those checks require the appropriate backend/product authority boundary.
54
67
  - Verification **never throws**. If the JWKS document is unreachable the
55
68
  request fails closed to unauthenticated (not a 500).
56
69
 
@@ -38,11 +38,18 @@ export interface JwksVerifierOptions {
38
38
  /** Optional structured logger. */
39
39
  logger?: SentinelLogger;
40
40
  }
41
+ /** Signature-verified current Sentinel access claims, not revocation or membership evidence. */
42
+ export interface VerifiedAccessToken extends DecodedToken {
43
+ iss: 'nexartis';
44
+ aud: 'nexartis';
45
+ jti: string;
46
+ nbf?: number;
47
+ }
41
48
  export type TokenVerifyOutcome =
42
49
  /** Signature valid, payload structurally sound and not expired. */
43
50
  {
44
51
  status: 'valid';
45
- payload: DecodedToken;
52
+ payload: VerifiedAccessToken;
46
53
  }
47
54
  /**
48
55
  * Token rejected: bad signature, malformed token, unsupported alg,
@@ -1 +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"}
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,gGAAgG;AAChG,MAAM,WAAW,mBAAoB,SAAQ,YAAY;IACxD,GAAG,EAAE,UAAU,CAAC;IAChB,GAAG,EAAE,UAAU,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,kBAAkB;AAC7B,mEAAmE;AACjE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAE;AACnD;;;;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;AA4BD,6CAA6C;AAC7C,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C;AAgKD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,mBAAmB,GAAG,YAAY,CAmFjF"}
package/dist/core/jwks.js CHANGED
@@ -29,6 +29,15 @@ function base64UrlDecodeToString(input) {
29
29
  return new TextDecoder('utf-8').decode(base64UrlDecodeToBytes(input));
30
30
  }
31
31
  async function importPublicEd25519Key(jwk) {
32
+ // Optional JWK metadata must not contradict the key's advertised verification purpose.
33
+ if ('alg' in jwk && jwk.alg !== 'EdDSA')
34
+ return null;
35
+ if ('use' in jwk && jwk.use !== 'sig')
36
+ return null;
37
+ if ('key_ops' in jwk && (!Array.isArray(jwk.key_ops) ||
38
+ !jwk.key_ops.every((operation) => typeof operation === 'string') ||
39
+ !jwk.key_ops.includes('verify')))
40
+ return null;
32
41
  if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || typeof jwk.x !== 'string' || jwk.x.length === 0) {
33
42
  return null;
34
43
  }
@@ -121,27 +130,35 @@ function parseTokenHeader(token) {
121
130
  return null;
122
131
  }
123
132
  }
133
+ function isSafeNumericDate(value) {
134
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 &&
135
+ value * 1000 <= Number.MAX_SAFE_INTEGER;
136
+ }
124
137
  function parseAndValidatePayload(payloadPart) {
125
138
  try {
126
139
  const payload = JSON.parse(base64UrlDecodeToString(payloadPart));
127
- if (typeof payload !== 'object' || payload === null)
140
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload))
141
+ return null;
142
+ // Current backend contract is a fixed service tuple, not a client/tenant audience.
143
+ if (payload.iss !== 'nexartis' || payload.aud !== 'nexartis')
128
144
  return null;
129
145
  if (typeof payload.sub !== 'string' || payload.sub.length === 0)
130
146
  return null;
131
147
  if (typeof payload.email !== 'string' || payload.email.length === 0)
132
148
  return null;
133
- if (typeof payload.exp !== 'number' || isNaN(payload.exp))
149
+ if (typeof payload.jti !== 'string' || payload.jti.length === 0)
150
+ return null;
151
+ if (!isSafeNumericDate(payload.exp) || !isSafeNumericDate(payload.iat))
134
152
  return null;
135
- if (typeof payload.iat !== 'number' || isNaN(payload.iat))
153
+ if ('nbf' in payload && !isSafeNumericDate(payload.nbf))
136
154
  return null;
155
+ if (!Array.isArray(payload.roles) || !payload.roles.every((role) => typeof role === 'string'))
156
+ return null;
157
+ // The signature covers these original claims. Do not normalize or silently drop roles.
137
158
  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
159
+ iss: payload.iss, aud: payload.aud, sub: payload.sub, email: payload.email,
160
+ jti: payload.jti, roles: payload.roles, iat: payload.iat, exp: payload.exp,
161
+ ...('nbf' in payload ? { nbf: payload.nbf } : {})
145
162
  };
146
163
  }
147
164
  catch {
@@ -213,6 +230,9 @@ export function createJwksVerifier(userOptions) {
213
230
  if (Date.now() >= payload.exp * 1000) {
214
231
  return { status: 'invalid', reason: 'token expired' };
215
232
  }
233
+ if (payload.nbf !== undefined && Date.now() < payload.nbf * 1000) {
234
+ return { status: 'invalid', reason: 'token not yet valid' };
235
+ }
216
236
  return { status: 'valid', payload };
217
237
  }
218
238
  catch (error) {
@@ -1 +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"]}
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\n/** Signature-verified current Sentinel access claims, not revocation or membership evidence. */\nexport interface VerifiedAccessToken extends DecodedToken {\n\tiss: 'nexartis';\n\taud: 'nexartis';\n\tjti: string;\n\tnbf?: number;\n}\n\nexport type TokenVerifyOutcome =\n\t/** Signature valid, payload structurally sound and not expired. */\n\t| { status: 'valid'; payload: VerifiedAccessToken }\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\tkey_ops?: 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\t// Optional JWK metadata must not contradict the key's advertised verification purpose.\n\tif ('alg' in jwk && jwk.alg !== 'EdDSA') return null;\n\tif ('use' in jwk && jwk.use !== 'sig') return null;\n\tif ('key_ops' in jwk && (!Array.isArray(jwk.key_ops) ||\n\t\t!jwk.key_ops.every((operation: unknown) => typeof operation === 'string') ||\n\t\t!jwk.key_ops.includes('verify'))) return 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 isSafeNumericDate(value: unknown): value is number {\n\treturn typeof value === 'number' && Number.isFinite(value) && value >= 0 &&\n\t\tvalue * 1000 <= Number.MAX_SAFE_INTEGER;\n}\n\nfunction parseAndValidatePayload(payloadPart: string): VerifiedAccessToken | null {\n\ttry {\n\t\tconst payload = JSON.parse(base64UrlDecodeToString(payloadPart));\n\t\tif (typeof payload !== 'object' || payload === null || Array.isArray(payload)) return null;\n\t\t// Current backend contract is a fixed service tuple, not a client/tenant audience.\n\t\tif (payload.iss !== 'nexartis' || payload.aud !== 'nexartis') 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.jti !== 'string' || payload.jti.length === 0) return null;\n\t\tif (!isSafeNumericDate(payload.exp) || !isSafeNumericDate(payload.iat)) return null;\n\t\tif ('nbf' in payload && !isSafeNumericDate(payload.nbf)) return null;\n\t\tif (!Array.isArray(payload.roles) || !payload.roles.every((role: unknown) => typeof role === 'string')) return null;\n\t\t// The signature covers these original claims. Do not normalize or silently drop roles.\n\t\treturn {\n\t\t\tiss: payload.iss, aud: payload.aud, sub: payload.sub, email: payload.email,\n\t\t\tjti: payload.jti, roles: payload.roles, iat: payload.iat, exp: payload.exp,\n\t\t\t...('nbf' in payload ? { nbf: payload.nbf } : {})\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\tif (payload.nbf !== undefined && Date.now() < payload.nbf * 1000) {\n\t\t\t\t\treturn { status: 'invalid', reason: 'token not yet valid' };\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"]}
@@ -115,6 +115,7 @@ export interface SentinelResponse {
115
115
  error?: string;
116
116
  [key: string]: unknown;
117
117
  }
118
+ /** Decoded/API payload shape only: this type does not prove signature or claim verification. */
118
119
  export interface DecodedToken {
119
120
  sub: string;
120
121
  email: string;
@@ -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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"}
@@ -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\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: 600 (10 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\n/** Decoded/API payload shape only: this type does not prove signature or claim verification. */\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: 600 (10 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexartis/sentinel-sdk",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "Passwordless magic-link authentication SDK for SvelteKit — client for the hosted Nexartis Sentinel auth backend.",
5
5
  "repository": {
6
6
  "type": "git",