@nexartis/sentinel-sdk 1.9.2 → 1.10.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.
Files changed (36) hide show
  1. package/README.md +32 -8
  2. package/dist/core/auth-utils.d.ts.map +1 -1
  3. package/dist/core/auth-utils.js +23 -3
  4. package/dist/core/auth-utils.js.map +1 -1
  5. package/dist/core/client.d.ts +4 -0
  6. package/dist/core/client.d.ts.map +1 -1
  7. package/dist/core/client.js +6 -0
  8. package/dist/core/client.js.map +1 -1
  9. package/dist/core/consumer-version-floor.d.ts +56 -0
  10. package/dist/core/consumer-version-floor.d.ts.map +1 -0
  11. package/dist/core/consumer-version-floor.js +233 -0
  12. package/dist/core/consumer-version-floor.js.map +1 -0
  13. package/dist/core/index.d.ts +2 -0
  14. package/dist/core/index.d.ts.map +1 -1
  15. package/dist/core/index.js +1 -0
  16. package/dist/core/index.js.map +1 -1
  17. package/dist/core/jwks.d.ts +8 -1
  18. package/dist/core/jwks.d.ts.map +1 -1
  19. package/dist/core/jwks.js +28 -4
  20. package/dist/core/jwks.js.map +1 -1
  21. package/dist/core/types.d.ts +35 -2
  22. package/dist/core/types.d.ts.map +1 -1
  23. package/dist/core/types.js.map +1 -1
  24. package/dist/sveltekit/cookies.d.ts +10 -0
  25. package/dist/sveltekit/cookies.d.ts.map +1 -1
  26. package/dist/sveltekit/cookies.js +13 -0
  27. package/dist/sveltekit/cookies.js.map +1 -1
  28. package/dist/sveltekit/handle.js +8 -2
  29. package/dist/sveltekit/handle.js.map +1 -1
  30. package/dist/sveltekit/handlers/index.d.ts.map +1 -1
  31. package/dist/sveltekit/handlers/index.js +30 -5
  32. package/dist/sveltekit/handlers/index.js.map +1 -1
  33. package/dist/sveltekit/pages/index.d.ts.map +1 -1
  34. package/dist/sveltekit/pages/index.js +21 -10
  35. package/dist/sveltekit/pages/index.js.map +1 -1
  36. package/package.json +8 -3
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @nexartis/sentinel-sdk
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@nexartis/sentinel-sdk.svg)](https://www.npmjs.com/package/@nexartis/sentinel-sdk)
4
- [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
5
5
  [![Node](https://img.shields.io/node/v/@nexartis/sentinel-sdk.svg)](https://nodejs.org)
6
6
 
7
7
  > **Passwordless, magic-link authentication for SvelteKit.** A batteries-included client SDK for the hosted Nexartis **Sentinel** auth backend — five lines in `hooks.server.ts`, three lines in your auth route, and you're done.
@@ -14,6 +14,7 @@ Sentinel is a passwordless authentication service: users sign in with a one-time
14
14
 
15
15
  - [What you get](#what-you-get)
16
16
  - [Security: signature verification](#security-signature-verification)
17
+ - [Consumers](#consumers)
17
18
  - [Install](#install)
18
19
  - [Configure](#configure)
19
20
  - [Quickstart](#quickstart)
@@ -53,11 +54,14 @@ downstream authorization gates.
53
54
  rejected outright.
54
55
  - Since **1.9.2**, signed claims must match the backend's current exact
55
56
  `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
+ nonempty strings. `roles` is now OPTIONAL: a post-TRUSTIP-147 identity token
58
+ omits the claim (the SDK defaults it to `[]`) and instead carries
59
+ `platform_admin` (boolean, surfaced as `platformAdmin`). `iat`, `exp`, and
57
60
  optional `nbf` must be finite nonnegative seconds (including fractional
58
61
  seconds) with bounded millisecond conversion. Expired tokens and tokens before `nbf` are rejected.
59
62
  - Valid verifier outcomes return only the qualified issuer, audience, subject,
60
- email, roles, JTI and NumericDate claims. Unqualified additional claims do not
63
+ email, roles (defaulted to `[]` when the token omits the claim),
64
+ `platform_admin`, JTI and NumericDate claims. Unqualified additional claims do not
61
65
  acquire tenant, client, membership, or human-presence semantics.
62
66
  `DecodedToken` and `decodeTokenLocally` alone are **not** verification evidence.
63
67
  - Local JWKS verification **does not query backend JTI revocation**, current
@@ -85,6 +89,22 @@ release closes.
85
89
 
86
90
  ---
87
91
 
92
+ ## Consumers
93
+
94
+ The downstream consumers of this SDK are enumerated in
95
+ [`consumers.json`](./consumers.json) and [`docs/CONSUMERS.md`](./docs/CONSUMERS.md),
96
+ together with the **`1.8.0` version floor** — the first line that verifies
97
+ signatures, so no in-scope consumer may resolve below it.
98
+
99
+ - `pnpm run gate:consumers` (also part of `pnpm run validate`) fails closed when
100
+ an in-scope consumer resolves below the floor; CI runs it across the fleet in
101
+ [`.github/workflows/consumer-version-floor.yml`](./.github/workflows/consumer-version-floor.yml).
102
+ - Every consumer must ship a **fail-closed forged-cookie test** that drives the
103
+ real handle with a real Ed25519 signature — not a mock. See
104
+ [`docs/CONSUMERS.md#fail-closed-forged-cookie-test`](./docs/CONSUMERS.md#fail-closed-forged-cookie-test).
105
+
106
+ ---
107
+
88
108
  ## Install
89
109
 
90
110
  ```bash
@@ -114,9 +134,13 @@ The SDK does not bake in any backend URL. Every request is directed at the URL *
114
134
 
115
135
  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`.
116
136
 
137
+ ### Environment contract
138
+
139
+ The fleet standardises on a **single** env name per environment: `SENTINEL_API_URL`, whose value carries the `/api` suffix (prod `https://sentinel.ocmeregistry.com/api`, dev `https://dev-sentinel.ocmeregistry.com/api`). The SDK reads only that name — the legacy `VITE_SENTINEL_API_URL` alias is **not** read — and there is **no** hardcoded fallback: a missing value fails loud with `SENTINEL_API_URL must be configured`.
140
+
117
141
  ```bash
118
142
  # .env
119
- SENTINEL_API_URL="https://auth.example.com"
143
+ SENTINEL_API_URL="https://auth.example.com/api"
120
144
  PUBLIC_BASE_URL="https://app.example.com"
121
145
  ```
122
146
 
@@ -140,7 +164,7 @@ export const handle = createSentinelHandle({
140
164
  });
141
165
  ```
142
166
 
143
- After this hook runs, `event.locals.user` is populated on every request with `{ isAuthenticated, id, email, roles }`.
167
+ After this hook runs, `event.locals.user` is populated on every request with `{ isAuthenticated, id, email, roles, platformAdmin }`.
144
168
 
145
169
  ### 2. Mount the auth API
146
170
 
@@ -247,7 +271,7 @@ Either way, this SDK is fully self-service and works against any Sentinel-compat
247
271
  | Framework-agnostic core | ✅ Use anywhere — Node, Bun, Cloudflare Workers, browsers |
248
272
  | Next.js / Remix / Nuxt / SolidStart | Roadmap — core is designed to make new adapters straightforward |
249
273
 
250
- Contributions of new adapters are welcome under the same Apache-2.0 license.
274
+ Contributions of new adapters are welcome; see [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the contribution terms.
251
275
 
252
276
  ---
253
277
 
@@ -259,6 +283,6 @@ This SDK follows [Semantic Versioning](https://semver.org/) and a strict **never
259
283
 
260
284
  ## License
261
285
 
262
- Licensed under the [Apache License 2.0](./LICENSE). Copyright © Nexartis LLC.
286
+ Licensed under the Apache License, Version 2.0 — see [`LICENSE`](./LICENSE) and [`NOTICE`](./NOTICE). Copyright © Nexartis, LLC. All Rights Reserved.
263
287
 
264
- You may use, modify, and redistribute this SDK freely in commercial and non-commercial projects. The Sentinel hosted backend is a separate service governed by its own terms — see <https://cubicube.com>.
288
+ This SDK is a deliberately public, permissively licensed package distributed under the Apache License, Version 2.0. The Sentinel hosted backend is a separate service governed by its own terms — see <https://cubicube.com>.
@@ -1 +1 @@
1
- {"version":3,"file":"auth-utils.d.ts","sourceRoot":"","sources":["../../src/core/auth-utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAuB,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAsBpF;;;;;;GAMG;AACH,wBAAsB,aAAa,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,UAAU,CAAC,KAAK,EAChC,MAAM,EAAE,cAAc,GACpB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAwB9B;AAiBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAyBrE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,GAAE,MAAW,GAAG,OAAO,CAkBjF"}
1
+ {"version":3,"file":"auth-utils.d.ts","sourceRoot":"","sources":["../../src/core/auth-utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAuB,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAsCpF;;;;;;GAMG;AACH,wBAAsB,aAAa,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,OAAO,UAAU,CAAC,KAAK,EAChC,MAAM,EAAE,cAAc,GACpB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAwB9B;AAiBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA8BrE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,GAAE,MAAW,GAAG,OAAO,CAkBjF"}
@@ -11,7 +11,22 @@ function hydrateTokenFromCurrentUser(payload, profile) {
11
11
  ...payload,
12
12
  sub: typeof profile.id === 'string' && profile.id.length > 0 ? profile.id : payload.sub,
13
13
  email: typeof profile.email === 'string' && profile.email.length > 0 ? profile.email : payload.email,
14
- roles: roles.length > 0 ? roles : payload.roles
14
+ roles: roles.length > 0 ? roles : Array.isArray(payload.roles) ? payload.roles : []
15
+ };
16
+ }
17
+ /**
18
+ * Normalize `roles` to a defined array on EVERY return path.
19
+ *
20
+ * TRUSTIP-147 removed the `roles` claim from the identity token, so a decoded
21
+ * payload may arrive without `roles`. Existing JavaScript consumers rely on the
22
+ * previous defined-array contract (e.g. `result.roles.includes(...)`), so an
23
+ * absent array must never leak out of `validateToken()` — it is normalized to
24
+ * `[]` rather than exposing `undefined`.
25
+ */
26
+ function normalizeDecodedRoles(payload) {
27
+ return {
28
+ ...payload,
29
+ roles: Array.isArray(payload.roles) ? payload.roles : []
15
30
  };
16
31
  }
17
32
  /**
@@ -28,14 +43,14 @@ export async function validateToken(accessToken, fetchFn, config) {
28
43
  if (result.code === 0 && result.valid && result.payload) {
29
44
  const payload = result.payload;
30
45
  if (config.hydrateUserProfile !== true) {
31
- return payload;
46
+ return normalizeDecodedRoles(payload);
32
47
  }
33
48
  try {
34
49
  const currentUser = await sentinel.getCurrentUser(accessToken);
35
50
  return hydrateTokenFromCurrentUser(payload, currentUser);
36
51
  }
37
52
  catch {
38
- return payload;
53
+ return normalizeDecodedRoles(payload);
39
54
  }
40
55
  }
41
56
  return null;
@@ -92,7 +107,12 @@ export function decodeTokenLocally(token) {
92
107
  return {
93
108
  sub: payload.sub,
94
109
  email: payload.email,
110
+ // TRUSTIP-147: `roles` is optional on post-cutover tokens; surface a
111
+ // defined array either way.
95
112
  roles: Array.isArray(payload.roles) ? payload.roles.filter((r) => typeof r === 'string') : [],
113
+ // Carry the identity-only admin flag through the local-decode path so
114
+ // `platformAdmin` is populated wherever locals.user is built.
115
+ ...(typeof payload.platform_admin === 'boolean' ? { platform_admin: payload.platform_admin } : {}),
96
116
  exp: payload.exp,
97
117
  iat: payload.iat
98
118
  };
@@ -1 +1 @@
1
- {"version":3,"file":"auth-utils.js","sourceRoot":"","sources":["../../src/core/auth-utils.ts"],"names":[],"mappings":"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","sourcesContent":["// src/core/auth-utils.ts\nimport type { CurrentUserResponse, DecodedToken, SentinelConfig } from './types.js';\nimport { SentinelClient } from './client.js';\n\nfunction normalizeRoles(profile: CurrentUserResponse): string[] {\n\tif (Array.isArray(profile.roles)) {\n\t\treturn profile.roles.filter((role): role is string => typeof role === 'string' && role.length > 0);\n\t}\n\n\treturn typeof profile.role === 'string' && profile.role.length > 0 ? [profile.role] : [];\n}\n\nfunction hydrateTokenFromCurrentUser(payload: DecodedToken, profile: CurrentUserResponse): DecodedToken {\n\tconst roles = normalizeRoles(profile);\n\n\treturn {\n\t\t...payload,\n\t\tsub: typeof profile.id === 'string' && profile.id.length > 0 ? profile.id : payload.sub,\n\t\temail: typeof profile.email === 'string' && profile.email.length > 0 ? profile.email : payload.email,\n\t\troles: roles.length > 0 ? roles : payload.roles\n\t};\n}\n\n/**\n * Validates a JWT access token using Sentinel API\n * @param accessToken - The JWT access token to validate\n * @param fetchFn - Fetch function (for server-side requests)\n * @param config - Sentinel configuration\n * @returns Decoded token data if valid, null if invalid\n */\nexport async function validateToken(\n\taccessToken: string,\n\tfetchFn: typeof globalThis.fetch,\n\tconfig: SentinelConfig\n): Promise<DecodedToken | null> {\n\ttry {\n\t\tconst sentinel = new SentinelClient(config, fetchFn);\n\t\tconst result = await sentinel.validateToken(accessToken);\n\n\t\tif (result.code === 0 && result.valid && result.payload) {\n\t\t\tconst payload = result.payload as DecodedToken;\n\t\t\tif (config.hydrateUserProfile !== true) {\n\t\t\t\treturn payload;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst currentUser = await sentinel.getCurrentUser(accessToken);\n\t\t\t\treturn hydrateTokenFromCurrentUser(payload, currentUser);\n\t\t\t} catch {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t} catch (error) {\n\t\tconsole.error('Token validation error:', error);\n\t\treturn null;\n\t}\n}\n\n/**\n * Decode a base64url-encoded string (JWT standard encoding).\n * Converts base64url → base64, adds padding, then decodes.\n */\nfunction base64UrlDecode(str: string): string {\n\tlet base64 = str.replace(/-/g, '+').replace(/_/g, '/');\n\tconst pad = base64.length % 4;\n\tif (pad) base64 += '='.repeat(4 - pad);\n\t// Decode via Uint8Array + TextDecoder for proper UTF-8 handling.\n\t// atob() returns a binary string that corrupts multi-byte characters.\n\tconst binary = atob(base64);\n\tconst bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));\n\treturn new TextDecoder('utf-8').decode(bytes);\n}\n\n/**\n * Decode JWT payload locally without API validation.\n * Used for per-request user extraction after local expiry check passes.\n *\n * Security model: tokens live in httpOnly/secure cookies and are validated\n * by the Sentinel API at issuance (claim) and refresh. The per-request\n * decode simply extracts the already-validated user info. A structurally\n * invalid or expired token falls through to the refresh path which does\n * a full API-validated exchange.\n *\n * @param token - JWT access token\n * @returns Decoded token payload if structurally valid and has required fields, null otherwise\n */\nexport function decodeTokenLocally(token: string): DecodedToken | null {\n\ttry {\n\t\tconst parts = token.split('.');\n\t\tif (parts.length !== 3) return null;\n\n\t\tconst payload = JSON.parse(base64UrlDecode(parts[1]));\n\n\t\t// Require sub and email to be strings (not just truthy) per DecodedToken contract\n\t\tif (typeof payload.sub !== 'string' || !payload.sub) return null;\n\t\tif (typeof payload.email !== 'string' || !payload.email) return null;\n\t\tif (typeof payload.exp !== 'number' || isNaN(payload.exp)) return null;\n\n\t\t// Validate iat is numeric (required by DecodedToken contract)\n\t\tif (typeof payload.iat !== 'number' || isNaN(payload.iat)) return null;\n\n\t\treturn {\n\t\t\tsub: payload.sub,\n\t\t\temail: payload.email,\n\t\t\troles: Array.isArray(payload.roles) ? payload.roles.filter((r: unknown): r is string => typeof r === 'string') : [],\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 * Check token expiration locally (saves API call)\n * @param token - JWT token to check\n * @param bufferSeconds - Seconds before actual expiry to consider expired (default: 30)\n * @returns true if token is expired or will expire within buffer, false otherwise\n */\nexport function isTokenExpired(token: string, bufferSeconds: number = 30): boolean {\n\ttry {\n\t\tconst parts = token.split('.');\n\t\tif (parts.length !== 3) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst payload = JSON.parse(base64UrlDecode(parts[1]));\n\t\tif (typeof payload.exp !== 'number' || isNaN(payload.exp)) {\n\t\t\treturn true;\n\t\t}\n\t\tconst expiresAt = payload.exp * 1000;\n\t\tconst bufferMs = bufferSeconds * 1000;\n\n\t\treturn Date.now() >= expiresAt - bufferMs;\n\t} catch {\n\t\treturn true; // Treat malformed tokens as expired\n\t}\n}\n\n"]}
1
+ {"version":3,"file":"auth-utils.js","sourceRoot":"","sources":["../../src/core/auth-utils.ts"],"names":[],"mappings":"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","sourcesContent":["// src/core/auth-utils.ts\nimport type { CurrentUserResponse, DecodedToken, SentinelConfig } from './types.js';\nimport { SentinelClient } from './client.js';\n\nfunction normalizeRoles(profile: CurrentUserResponse): string[] {\n\tif (Array.isArray(profile.roles)) {\n\t\treturn profile.roles.filter((role): role is string => typeof role === 'string' && role.length > 0);\n\t}\n\n\treturn typeof profile.role === 'string' && profile.role.length > 0 ? [profile.role] : [];\n}\n\nfunction hydrateTokenFromCurrentUser(payload: DecodedToken, profile: CurrentUserResponse): DecodedToken {\n\tconst roles = normalizeRoles(profile);\n\n\treturn {\n\t\t...payload,\n\t\tsub: typeof profile.id === 'string' && profile.id.length > 0 ? profile.id : payload.sub,\n\t\temail: typeof profile.email === 'string' && profile.email.length > 0 ? profile.email : payload.email,\n\t\troles: roles.length > 0 ? roles : Array.isArray(payload.roles) ? payload.roles : []\n\t};\n}\n\n/**\n * Normalize `roles` to a defined array on EVERY return path.\n *\n * TRUSTIP-147 removed the `roles` claim from the identity token, so a decoded\n * payload may arrive without `roles`. Existing JavaScript consumers rely on the\n * previous defined-array contract (e.g. `result.roles.includes(...)`), so an\n * absent array must never leak out of `validateToken()` — it is normalized to\n * `[]` rather than exposing `undefined`.\n */\nfunction normalizeDecodedRoles(payload: DecodedToken): DecodedToken {\n\treturn {\n\t\t...payload,\n\t\troles: Array.isArray(payload.roles) ? payload.roles : []\n\t};\n}\n\n/**\n * Validates a JWT access token using Sentinel API\n * @param accessToken - The JWT access token to validate\n * @param fetchFn - Fetch function (for server-side requests)\n * @param config - Sentinel configuration\n * @returns Decoded token data if valid, null if invalid\n */\nexport async function validateToken(\n\taccessToken: string,\n\tfetchFn: typeof globalThis.fetch,\n\tconfig: SentinelConfig\n): Promise<DecodedToken | null> {\n\ttry {\n\t\tconst sentinel = new SentinelClient(config, fetchFn);\n\t\tconst result = await sentinel.validateToken(accessToken);\n\n\t\tif (result.code === 0 && result.valid && result.payload) {\n\t\t\tconst payload = result.payload as DecodedToken;\n\t\t\tif (config.hydrateUserProfile !== true) {\n\t\t\t\treturn normalizeDecodedRoles(payload);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst currentUser = await sentinel.getCurrentUser(accessToken);\n\t\t\t\treturn hydrateTokenFromCurrentUser(payload, currentUser);\n\t\t\t} catch {\n\t\t\t\treturn normalizeDecodedRoles(payload);\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t} catch (error) {\n\t\tconsole.error('Token validation error:', error);\n\t\treturn null;\n\t}\n}\n\n/**\n * Decode a base64url-encoded string (JWT standard encoding).\n * Converts base64url → base64, adds padding, then decodes.\n */\nfunction base64UrlDecode(str: string): string {\n\tlet base64 = str.replace(/-/g, '+').replace(/_/g, '/');\n\tconst pad = base64.length % 4;\n\tif (pad) base64 += '='.repeat(4 - pad);\n\t// Decode via Uint8Array + TextDecoder for proper UTF-8 handling.\n\t// atob() returns a binary string that corrupts multi-byte characters.\n\tconst binary = atob(base64);\n\tconst bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));\n\treturn new TextDecoder('utf-8').decode(bytes);\n}\n\n/**\n * Decode JWT payload locally without API validation.\n * Used for per-request user extraction after local expiry check passes.\n *\n * Security model: tokens live in httpOnly/secure cookies and are validated\n * by the Sentinel API at issuance (claim) and refresh. The per-request\n * decode simply extracts the already-validated user info. A structurally\n * invalid or expired token falls through to the refresh path which does\n * a full API-validated exchange.\n *\n * @param token - JWT access token\n * @returns Decoded token payload if structurally valid and has required fields, null otherwise\n */\nexport function decodeTokenLocally(token: string): DecodedToken | null {\n\ttry {\n\t\tconst parts = token.split('.');\n\t\tif (parts.length !== 3) return null;\n\n\t\tconst payload = JSON.parse(base64UrlDecode(parts[1]));\n\n\t\t// Require sub and email to be strings (not just truthy) per DecodedToken contract\n\t\tif (typeof payload.sub !== 'string' || !payload.sub) return null;\n\t\tif (typeof payload.email !== 'string' || !payload.email) return null;\n\t\tif (typeof payload.exp !== 'number' || isNaN(payload.exp)) return null;\n\n\t\t// Validate iat is numeric (required by DecodedToken contract)\n\t\tif (typeof payload.iat !== 'number' || isNaN(payload.iat)) return null;\n\n\t\treturn {\n\t\t\tsub: payload.sub,\n\t\t\temail: payload.email,\n\t\t\t// TRUSTIP-147: `roles` is optional on post-cutover tokens; surface a\n\t\t\t// defined array either way.\n\t\t\troles: Array.isArray(payload.roles) ? payload.roles.filter((r: unknown): r is string => typeof r === 'string') : [],\n\t\t\t// Carry the identity-only admin flag through the local-decode path so\n\t\t\t// `platformAdmin` is populated wherever locals.user is built.\n\t\t\t...(typeof payload.platform_admin === 'boolean' ? { platform_admin: payload.platform_admin } : {}),\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 * Check token expiration locally (saves API call)\n * @param token - JWT token to check\n * @param bufferSeconds - Seconds before actual expiry to consider expired (default: 30)\n * @returns true if token is expired or will expire within buffer, false otherwise\n */\nexport function isTokenExpired(token: string, bufferSeconds: number = 30): boolean {\n\ttry {\n\t\tconst parts = token.split('.');\n\t\tif (parts.length !== 3) {\n\t\t\treturn true;\n\t\t}\n\n\t\tconst payload = JSON.parse(base64UrlDecode(parts[1]));\n\t\tif (typeof payload.exp !== 'number' || isNaN(payload.exp)) {\n\t\t\treturn true;\n\t\t}\n\t\tconst expiresAt = payload.exp * 1000;\n\t\tconst bufferMs = bufferSeconds * 1000;\n\n\t\treturn Date.now() >= expiresAt - bufferMs;\n\t} catch {\n\t\treturn true; // Treat malformed tokens as expired\n\t}\n}\n\n"]}
@@ -12,6 +12,10 @@ export declare class SentinelClient {
12
12
  startLogin(email: string, options?: {
13
13
  redirectContext?: string;
14
14
  rememberMe?: boolean;
15
+ /** FanCastle mint tier (site_owner | fan | admin). */
16
+ audience?: 'site_owner' | 'fan' | 'admin';
17
+ /** FanCastle tenant id — community access token aud. */
18
+ communityId?: string;
15
19
  }): Promise<StartLoginResponse>;
16
20
  pollStatus(transactionId: string, clientToken?: string): Promise<PollResponse>;
17
21
  claimTokens(transactionId: string, clientToken: string): Promise<ClaimResponse>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/core/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,4BAA4B,EAC5B,MAAM,YAAY,CAAC;AAOpB,qBAAa,cAAc;IAC1B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,GAAG,CAAiB;gBAEhB,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,OAAO,UAAU,CAAC,KAAa;YAQ9D,OAAO;IAkErB,2CAA2C;IAC3C,OAAO,CAAC,OAAO;IAKT,UAAU,CACf,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAC1D,OAAO,CAAC,kBAAkB,CAAC;IAiBxB,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAW9E,WAAW,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAO/E,QAAQ,CACb,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,GACpC,OAAO,CAAC,gBAAgB,CAAC;IActB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAOnD,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAO/E,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAO5D,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAS7D,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAQvE"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/core/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,4BAA4B,EAC5B,MAAM,YAAY,CAAC;AAOpB,qBAAa,cAAc;IAC1B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,GAAG,CAAiB;gBAEhB,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,OAAO,UAAU,CAAC,KAAa;YAQ9D,OAAO;IAkErB,2CAA2C;IAC3C,OAAO,CAAC,OAAO;IAKT,UAAU,CACf,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QACT,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,sDAAsD;QACtD,QAAQ,CAAC,EAAE,YAAY,GAAG,KAAK,GAAG,OAAO,CAAC;QAC1C,wDAAwD;QACxD,WAAW,CAAC,EAAE,MAAM,CAAC;KACrB,GACC,OAAO,CAAC,kBAAkB,CAAC;IAuBxB,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAW9E,WAAW,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAO/E,QAAQ,CACb,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,GACpC,OAAO,CAAC,gBAAgB,CAAC;IActB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAOnD,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAO/E,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAO5D,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAS7D,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAQvE"}
@@ -87,6 +87,12 @@ export class SentinelClient {
87
87
  if (options?.rememberMe !== undefined) {
88
88
  body.remember_me = options.rememberMe;
89
89
  }
90
+ if (options?.audience !== undefined) {
91
+ body.audience = options.audience;
92
+ }
93
+ if (options?.communityId !== undefined) {
94
+ body.community_id = options.communityId;
95
+ }
90
96
  return this.request('/auth/login-passwordless', {
91
97
  method: 'POST',
92
98
  body: JSON.stringify(body)
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/core/client.ts"],"names":[],"mappings":"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","sourcesContent":["// src/core/client.ts\nimport type {\n\tSentinelConfig,\n\tStartLoginResponse,\n\tPollResponse,\n\tClaimResponse,\n\tRefreshResponse,\n\tValidateResponse,\n\tCurrentUserResponse,\n\tRegisterResponse,\n\tVerifyResponse,\n\tCompletePasswordlessResponse\n} from './types.js';\nimport { SentinelApiError, SentinelTimeoutError, isRetryableError, isRetryableStatus } from './errors.js';\nimport { createLogger, type SentinelLogger } from './logger.js';\n\nconst DEFAULT_TIMEOUT_MS = 8_000;\nconst DEFAULT_MAX_RETRIES = 1;\n\nexport class SentinelClient {\n\tprivate config: SentinelConfig;\n\tprivate fetch: typeof globalThis.fetch;\n\tprivate timeoutMs: number;\n\tprivate maxRetries: number;\n\tprivate log: SentinelLogger;\n\n\tconstructor(config: SentinelConfig, fetchFn: typeof globalThis.fetch = fetch) {\n\t\tthis.config = config;\n\t\tthis.fetch = fetchFn;\n\t\tthis.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\t\tthis.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n\t\tthis.log = config.logger ?? createLogger('sentinel-client');\n\t}\n\n\tprivate async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n\t\tconst url = `${this.config.apiUrl}${endpoint}`;\n\t\tconst headers = {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t...options.headers\n\t\t};\n\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n\t\t\ttry {\n\t\t\t\t// Apply per-request timeout via AbortController\n\t\t\t\tconst controller = new AbortController();\n\t\t\t\tconst timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await this.fetch(url, {\n\t\t\t\t\t\t...options,\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tsignal: controller.signal\n\t\t\t\t\t});\n\n\t\t\t\t\tclearTimeout(timeoutId);\n\n\t\t\t\t\t// If the response status is retryable and we have retries left, retry\n\t\t\t\t\tif (isRetryableStatus(response.status) && attempt < this.maxRetries) {\n\t\t\t\t\t\tlastError = new Error(`HTTP ${response.status} from ${url}`);\n\t\t\t\t\t\tawait this.backoff(attempt);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fail loud on any remaining non-2xx status. A non-ok body is\n\t\t\t\t\t// never returned as a resolved value; retryable statuses were\n\t\t\t\t\t// handled above and are only surfaced here once retries are\n\t\t\t\t\t// exhausted.\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\tthrow await SentinelApiError.fromResponse(response, url);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn await response.json();\n\t\t\t\t} catch (fetchError) {\n\t\t\t\t\tclearTimeout(timeoutId);\n\n\t\t\t\t\t// Convert AbortError to SentinelTimeoutError\n\t\t\t\t\tif (fetchError instanceof DOMException && fetchError.name === 'AbortError') {\n\t\t\t\t\t\tthrow new SentinelTimeoutError(url, this.timeoutMs);\n\t\t\t\t\t}\n\t\t\t\t\tthrow fetchError;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error;\n\n\t\t\t\t// Only retry retryable errors\n\t\t\t\tif (attempt < this.maxRetries && isRetryableError(error)) {\n\t\t\t\t\tawait this.backoff(attempt);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\t// Should not reach here, but just in case\n\t\tthrow lastError;\n\t}\n\n\t/** Exponential backoff: 1s, 2s, 4s, ... */\n\tprivate backoff(attempt: number): Promise<void> {\n\t\tconst delay = Math.min(1000 * Math.pow(2, attempt), 8000);\n\t\treturn new Promise(resolve => setTimeout(resolve, delay));\n\t}\n\n\tasync startLogin(\n\t\temail: string,\n\t\toptions?: { redirectContext?: string; rememberMe?: boolean }\n\t): Promise<StartLoginResponse> {\n\t\tconst body: Record<string, unknown> = {\n\t\t\temail,\n\t\t\temail_endpoint: this.config.baseUrl\n\t\t};\n\t\tif (options?.redirectContext !== undefined) {\n\t\t\tbody.redirect_context = options.redirectContext;\n\t\t}\n\t\tif (options?.rememberMe !== undefined) {\n\t\t\tbody.remember_me = options.rememberMe;\n\t\t}\n\t\treturn this.request<StartLoginResponse>('/auth/login-passwordless', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify(body)\n\t\t});\n\t}\n\n\tasync pollStatus(transactionId: string, clientToken?: string): Promise<PollResponse> {\n\t\tconst options: RequestInit = {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ transactionId })\n\t\t};\n\t\tif (clientToken !== undefined) {\n\t\t\toptions.headers = { 'Authorization': `Bearer ${clientToken}` };\n\t\t}\n\t\treturn this.request<PollResponse>('/auth/poll', options);\n\t}\n\n\tasync claimTokens(transactionId: string, clientToken: string): Promise<ClaimResponse> {\n\t\treturn this.request<ClaimResponse>('/auth/claim', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ transactionId, clientToken })\n\t\t});\n\t}\n\n\tasync register(\n\t\temail: string,\n\t\toptions?: { redirectContext?: string }\n\t): Promise<RegisterResponse> {\n\t\tconst body: Record<string, unknown> = {\n\t\t\temail,\n\t\t\temail_endpoint: this.config.baseUrl\n\t\t};\n\t\tif (options?.redirectContext !== undefined) {\n\t\t\tbody.redirect_context = options.redirectContext;\n\t\t}\n\t\treturn this.request<RegisterResponse>('/auth/register', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify(body)\n\t\t});\n\t}\n\n\tasync verifyEmail(token: string): Promise<VerifyResponse> {\n\t\treturn this.request<VerifyResponse>('/auth/verify', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ verification_token: token })\n\t\t});\n\t}\n\n\tasync completePasswordless(magicToken: string): Promise<CompletePasswordlessResponse> {\n\t\treturn this.request<CompletePasswordlessResponse>('/auth/complete-passwordless', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ magic_token: magicToken })\n\t\t});\n\t}\n\n\tasync refreshToken(refreshToken: string): Promise<RefreshResponse> {\n\t\treturn this.request<RefreshResponse>('/auth/refresh', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ refresh_token: refreshToken })\n\t\t});\n\t}\n\n\tasync validateToken(accessToken: string): Promise<ValidateResponse> {\n\t\treturn this.request<ValidateResponse>('/auth/validate', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Authorization': `Bearer ${accessToken}`\n\t\t\t}\n\t\t});\n\t}\n\n\tasync getCurrentUser(accessToken: string): Promise<CurrentUserResponse> {\n\t\treturn this.request<CurrentUserResponse>('/users/me', {\n\t\t\tmethod: 'GET',\n\t\t\theaders: {\n\t\t\t\t'Authorization': `Bearer ${accessToken}`\n\t\t\t}\n\t\t});\n\t}\n}\n\n"]}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/core/client.ts"],"names":[],"mappings":"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","sourcesContent":["// src/core/client.ts\nimport type {\n\tSentinelConfig,\n\tStartLoginResponse,\n\tPollResponse,\n\tClaimResponse,\n\tRefreshResponse,\n\tValidateResponse,\n\tCurrentUserResponse,\n\tRegisterResponse,\n\tVerifyResponse,\n\tCompletePasswordlessResponse\n} from './types.js';\nimport { SentinelApiError, SentinelTimeoutError, isRetryableError, isRetryableStatus } from './errors.js';\nimport { createLogger, type SentinelLogger } from './logger.js';\n\nconst DEFAULT_TIMEOUT_MS = 8_000;\nconst DEFAULT_MAX_RETRIES = 1;\n\nexport class SentinelClient {\n\tprivate config: SentinelConfig;\n\tprivate fetch: typeof globalThis.fetch;\n\tprivate timeoutMs: number;\n\tprivate maxRetries: number;\n\tprivate log: SentinelLogger;\n\n\tconstructor(config: SentinelConfig, fetchFn: typeof globalThis.fetch = fetch) {\n\t\tthis.config = config;\n\t\tthis.fetch = fetchFn;\n\t\tthis.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\t\tthis.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\n\t\tthis.log = config.logger ?? createLogger('sentinel-client');\n\t}\n\n\tprivate async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n\t\tconst url = `${this.config.apiUrl}${endpoint}`;\n\t\tconst headers = {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t...options.headers\n\t\t};\n\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n\t\t\ttry {\n\t\t\t\t// Apply per-request timeout via AbortController\n\t\t\t\tconst controller = new AbortController();\n\t\t\t\tconst timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await this.fetch(url, {\n\t\t\t\t\t\t...options,\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tsignal: controller.signal\n\t\t\t\t\t});\n\n\t\t\t\t\tclearTimeout(timeoutId);\n\n\t\t\t\t\t// If the response status is retryable and we have retries left, retry\n\t\t\t\t\tif (isRetryableStatus(response.status) && attempt < this.maxRetries) {\n\t\t\t\t\t\tlastError = new Error(`HTTP ${response.status} from ${url}`);\n\t\t\t\t\t\tawait this.backoff(attempt);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Fail loud on any remaining non-2xx status. A non-ok body is\n\t\t\t\t\t// never returned as a resolved value; retryable statuses were\n\t\t\t\t\t// handled above and are only surfaced here once retries are\n\t\t\t\t\t// exhausted.\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\tthrow await SentinelApiError.fromResponse(response, url);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn await response.json();\n\t\t\t\t} catch (fetchError) {\n\t\t\t\t\tclearTimeout(timeoutId);\n\n\t\t\t\t\t// Convert AbortError to SentinelTimeoutError\n\t\t\t\t\tif (fetchError instanceof DOMException && fetchError.name === 'AbortError') {\n\t\t\t\t\t\tthrow new SentinelTimeoutError(url, this.timeoutMs);\n\t\t\t\t\t}\n\t\t\t\t\tthrow fetchError;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error;\n\n\t\t\t\t// Only retry retryable errors\n\t\t\t\tif (attempt < this.maxRetries && isRetryableError(error)) {\n\t\t\t\t\tawait this.backoff(attempt);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\t// Should not reach here, but just in case\n\t\tthrow lastError;\n\t}\n\n\t/** Exponential backoff: 1s, 2s, 4s, ... */\n\tprivate backoff(attempt: number): Promise<void> {\n\t\tconst delay = Math.min(1000 * Math.pow(2, attempt), 8000);\n\t\treturn new Promise(resolve => setTimeout(resolve, delay));\n\t}\n\n\tasync startLogin(\n\t\temail: string,\n\t\toptions?: {\n\t\t\tredirectContext?: string;\n\t\t\trememberMe?: boolean;\n\t\t\t/** FanCastle mint tier (site_owner | fan | admin). */\n\t\t\taudience?: 'site_owner' | 'fan' | 'admin';\n\t\t\t/** FanCastle tenant id — community access token aud. */\n\t\t\tcommunityId?: string;\n\t\t}\n\t): Promise<StartLoginResponse> {\n\t\tconst body: Record<string, unknown> = {\n\t\t\temail,\n\t\t\temail_endpoint: this.config.baseUrl\n\t\t};\n\t\tif (options?.redirectContext !== undefined) {\n\t\t\tbody.redirect_context = options.redirectContext;\n\t\t}\n\t\tif (options?.rememberMe !== undefined) {\n\t\t\tbody.remember_me = options.rememberMe;\n\t\t}\n\t\tif (options?.audience !== undefined) {\n\t\t\tbody.audience = options.audience;\n\t\t}\n\t\tif (options?.communityId !== undefined) {\n\t\t\tbody.community_id = options.communityId;\n\t\t}\n\t\treturn this.request<StartLoginResponse>('/auth/login-passwordless', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify(body)\n\t\t});\n\t}\n\n\tasync pollStatus(transactionId: string, clientToken?: string): Promise<PollResponse> {\n\t\tconst options: RequestInit = {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ transactionId })\n\t\t};\n\t\tif (clientToken !== undefined) {\n\t\t\toptions.headers = { 'Authorization': `Bearer ${clientToken}` };\n\t\t}\n\t\treturn this.request<PollResponse>('/auth/poll', options);\n\t}\n\n\tasync claimTokens(transactionId: string, clientToken: string): Promise<ClaimResponse> {\n\t\treturn this.request<ClaimResponse>('/auth/claim', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ transactionId, clientToken })\n\t\t});\n\t}\n\n\tasync register(\n\t\temail: string,\n\t\toptions?: { redirectContext?: string }\n\t): Promise<RegisterResponse> {\n\t\tconst body: Record<string, unknown> = {\n\t\t\temail,\n\t\t\temail_endpoint: this.config.baseUrl\n\t\t};\n\t\tif (options?.redirectContext !== undefined) {\n\t\t\tbody.redirect_context = options.redirectContext;\n\t\t}\n\t\treturn this.request<RegisterResponse>('/auth/register', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify(body)\n\t\t});\n\t}\n\n\tasync verifyEmail(token: string): Promise<VerifyResponse> {\n\t\treturn this.request<VerifyResponse>('/auth/verify', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ verification_token: token })\n\t\t});\n\t}\n\n\tasync completePasswordless(magicToken: string): Promise<CompletePasswordlessResponse> {\n\t\treturn this.request<CompletePasswordlessResponse>('/auth/complete-passwordless', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ magic_token: magicToken })\n\t\t});\n\t}\n\n\tasync refreshToken(refreshToken: string): Promise<RefreshResponse> {\n\t\treturn this.request<RefreshResponse>('/auth/refresh', {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({ refresh_token: refreshToken })\n\t\t});\n\t}\n\n\tasync validateToken(accessToken: string): Promise<ValidateResponse> {\n\t\treturn this.request<ValidateResponse>('/auth/validate', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Authorization': `Bearer ${accessToken}`\n\t\t\t}\n\t\t});\n\t}\n\n\tasync getCurrentUser(accessToken: string): Promise<CurrentUserResponse> {\n\t\treturn this.request<CurrentUserResponse>('/users/me', {\n\t\t\tmethod: 'GET',\n\t\t\theaders: {\n\t\t\t\t'Authorization': `Bearer ${accessToken}`\n\t\t\t}\n\t\t});\n\t}\n}\n\n"]}
@@ -0,0 +1,56 @@
1
+ export declare const SENTINEL_SDK_PACKAGE = "@nexartis/sentinel-sdk";
2
+ /** First sentinel-sdk line that verifies signatures against the JWKS. */
3
+ export declare const SENTINEL_SDK_VERSION_FLOOR = "1.8.0";
4
+ export type PinKind = 'exact' | 'range' | 'tarball' | 'alias' | 'unresolved';
5
+ export interface ParsedPin {
6
+ kind: PinKind;
7
+ /** Lower-bound version the specifier guarantees, or null when unknown. */
8
+ version: string | null;
9
+ spec: string;
10
+ reason: string;
11
+ }
12
+ export type FloorStatus = 'ok' | 'below-floor' | 'unresolved' | 'absent';
13
+ export interface FloorVerdict {
14
+ status: FloorStatus;
15
+ /** Version the specifier is guaranteed to resolve at or above, when known. */
16
+ pinnedVersion: string | null;
17
+ spec: string | null;
18
+ reason: string;
19
+ }
20
+ export interface ConsumerEntry {
21
+ repo: string;
22
+ /** The declared @nexartis/sentinel-sdk specifier, or null when not declared. */
23
+ dependencySpec?: string | null;
24
+ }
25
+ export interface ConsumerVerdict extends FloorVerdict {
26
+ repo: string;
27
+ }
28
+ export interface FloorReport {
29
+ floor: string;
30
+ consumers: ConsumerVerdict[];
31
+ /** Repos that resolve below the floor — the hard gate failures. */
32
+ violations: string[];
33
+ /** Repos whose specifier cannot prove a floor (strict mode fails these). */
34
+ unresolved: string[];
35
+ /** True when no consumer resolves below the floor. */
36
+ ok: boolean;
37
+ /** True when no consumer is below the floor AND none is unresolved. */
38
+ strictOk: boolean;
39
+ }
40
+ /** Compare two semver strings. Returns -1, 0, or 1. Unparseable input throws. */
41
+ export declare function compareVersions(a: string, b: string): number;
42
+ /**
43
+ * Parse a declared dependency specifier into the lower-bound version it
44
+ * guarantees. Anything that cannot prove a published floor (workspace/file/
45
+ * git sources, floating tags, upper-bounded ranges) is reported `unresolved`
46
+ * rather than guessed — the gate fails closed on those in strict mode.
47
+ */
48
+ export declare function parsePinnedVersion(spec: string): ParsedPin;
49
+ /**
50
+ * Evaluate one declared specifier against the floor. A missing specifier is
51
+ * `absent` (not a consumer); an unprovable specifier is `unresolved`.
52
+ */
53
+ export declare function evaluatePin(spec: string | null | undefined, floor?: string): FloorVerdict;
54
+ /** Evaluate a set of consumers and summarise the fleet verdict. */
55
+ export declare function evaluateConsumers(entries: readonly ConsumerEntry[], floor?: string): FloorReport;
56
+ //# sourceMappingURL=consumer-version-floor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer-version-floor.d.ts","sourceRoot":"","sources":["../../src/core/consumer-version-floor.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAE7D,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,UAAU,CAAC;AAElD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,YAAY,CAAC;AAE7E,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,aAAa,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEzE,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB,8EAA8E;IAC9E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACpD,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,mEAAmE;IACnE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,sDAAsD;IACtD,EAAE,EAAE,OAAO,CAAC;IACZ,uEAAuE;IACvE,QAAQ,EAAE,OAAO,CAAC;CAClB;AAqDD,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAc5D;AAsCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CA+D1D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC/B,KAAK,GAAE,MAAmC,GACxC,YAAY,CA4Bd;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,KAAK,GAAE,MAAmC,GACxC,WAAW,CAeb"}
@@ -0,0 +1,233 @@
1
+ // src/core/consumer-version-floor.ts
2
+ //
3
+ // Fleet consumer version-floor gate for @nexartis/sentinel-sdk.
4
+ //
5
+ // 1.8.0 is the first line that verifies every access-token signature against
6
+ // the Sentinel JWKS on each request (the admin-gate audit fix). A consumer
7
+ // pinned below that floor trusts unverified token payloads, so the fleet must
8
+ // fail closed on any in-scope repo that resolves below it.
9
+ //
10
+ // This module is pure and dependency-free so the same logic backs the unit
11
+ // tests, the `scripts/check-consumer-version-floor.mjs` CLI gate, and CI.
12
+ export const SENTINEL_SDK_PACKAGE = '@nexartis/sentinel-sdk';
13
+ /** First sentinel-sdk line that verifies signatures against the JWKS. */
14
+ export const SENTINEL_SDK_VERSION_FLOOR = '1.8.0';
15
+ const SEMVER_RE = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
16
+ function toSemver(token) {
17
+ const match = SEMVER_RE.exec(token.trim());
18
+ if (!match)
19
+ return null;
20
+ return {
21
+ major: Number(match[1]),
22
+ minor: Number(match[2] ?? 0),
23
+ patch: Number(match[3] ?? 0),
24
+ prerelease: match[4] ?? null
25
+ };
26
+ }
27
+ /** Normalise a loose version token (`v1.9`, `1.9.2+build`) to `major.minor.patch[-pre]`. */
28
+ function normalizeVersion(token) {
29
+ const parsed = toSemver(token);
30
+ if (!parsed)
31
+ return null;
32
+ const base = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
33
+ return parsed.prerelease ? `${base}-${parsed.prerelease}` : base;
34
+ }
35
+ function comparePrerelease(a, b) {
36
+ const left = a.split('.');
37
+ const right = b.split('.');
38
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
39
+ const x = left[i];
40
+ const y = right[i];
41
+ if (x === undefined)
42
+ return -1;
43
+ if (y === undefined)
44
+ return 1;
45
+ const xn = /^\d+$/.test(x);
46
+ const yn = /^\d+$/.test(y);
47
+ if (xn && yn) {
48
+ const diff = Number(x) - Number(y);
49
+ if (diff !== 0)
50
+ return diff < 0 ? -1 : 1;
51
+ }
52
+ else if (xn !== yn) {
53
+ // Numeric identifiers always have lower precedence than alphanumeric ones.
54
+ return xn ? -1 : 1;
55
+ }
56
+ else if (x !== y) {
57
+ return x < y ? -1 : 1;
58
+ }
59
+ }
60
+ return 0;
61
+ }
62
+ /** Compare two semver strings. Returns -1, 0, or 1. Unparseable input throws. */
63
+ export function compareVersions(a, b) {
64
+ const left = toSemver(a);
65
+ const right = toSemver(b);
66
+ if (!left || !right) {
67
+ throw new Error(`compareVersions: not a version: "${!left ? a : b}"`);
68
+ }
69
+ for (const key of ['major', 'minor', 'patch']) {
70
+ if (left[key] !== right[key])
71
+ return left[key] < right[key] ? -1 : 1;
72
+ }
73
+ if (!left.prerelease && !right.prerelease)
74
+ return 0;
75
+ // A release outranks its own prereleases (1.8.0 > 1.8.0-beta.1).
76
+ if (!left.prerelease)
77
+ return 1;
78
+ if (!right.prerelease)
79
+ return -1;
80
+ return comparePrerelease(left.prerelease, right.prerelease);
81
+ }
82
+ const TARBALL_SUFFIX = '.tgz';
83
+ const TARBALL_MARKER = 'sentinel-sdk-';
84
+ function isVersionTokenChar(ch) {
85
+ return ((ch >= '0' && ch <= '9') ||
86
+ (ch >= 'A' && ch <= 'Z') ||
87
+ (ch >= 'a' && ch <= 'z') ||
88
+ ch === '.' ||
89
+ ch === '+' ||
90
+ ch === '-');
91
+ }
92
+ /**
93
+ * Extract the `<version>` from a `sentinel-sdk-<version>.tgz` tarball name by
94
+ * index slicing (strictly linear, no regex). The previous backtracking pattern
95
+ * `/sentinel-sdk-([0-9][0-9A-Za-z.+-]*)\.tgz/` let the version class and the
96
+ * literal `.tgz` both consume `.`, a polynomial-regex (ReDoS) shape on
97
+ * uncontrolled specifier input. Returns null when the specifier is not a
98
+ * sentinel-sdk tarball name.
99
+ */
100
+ function extractTarballVersion(raw) {
101
+ if (!raw.endsWith(TARBALL_SUFFIX))
102
+ return null;
103
+ const stem = raw.slice(0, raw.length - TARBALL_SUFFIX.length);
104
+ const markerAt = stem.lastIndexOf(TARBALL_MARKER);
105
+ if (markerAt < 0)
106
+ return null;
107
+ const candidate = stem.slice(markerAt + TARBALL_MARKER.length);
108
+ if (candidate.length === 0)
109
+ return null;
110
+ if (candidate[0] < '0' || candidate[0] > '9')
111
+ return null;
112
+ for (const ch of candidate) {
113
+ if (!isVersionTokenChar(ch))
114
+ return null;
115
+ }
116
+ return candidate;
117
+ }
118
+ /**
119
+ * Parse a declared dependency specifier into the lower-bound version it
120
+ * guarantees. Anything that cannot prove a published floor (workspace/file/
121
+ * git sources, floating tags, upper-bounded ranges) is reported `unresolved`
122
+ * rather than guessed — the gate fails closed on those in strict mode.
123
+ */
124
+ export function parsePinnedVersion(spec) {
125
+ const raw = (spec ?? '').trim();
126
+ const unresolved = (reason) => ({
127
+ kind: 'unresolved',
128
+ version: null,
129
+ spec: raw,
130
+ reason
131
+ });
132
+ if (!raw)
133
+ return unresolved('empty specifier');
134
+ if (/^(workspace|file|link|portal):/i.test(raw)) {
135
+ return unresolved(`local source (${raw.split(':')[0]}:) — cannot prove a published floor`);
136
+ }
137
+ if (/^(git|github)(\+|:)/i.test(raw) || /\.git(#|$)/.test(raw)) {
138
+ return unresolved('git source — cannot prove a published floor');
139
+ }
140
+ // npm alias, e.g. `npm:@nexartis/sentinel-sdk@1.9.0`.
141
+ if (raw.startsWith('npm:')) {
142
+ const rest = raw.slice('npm:'.length);
143
+ const at = rest.lastIndexOf('@');
144
+ const range = at > 0 ? rest.slice(at + 1) : '';
145
+ const inner = parsePinnedVersion(range);
146
+ return {
147
+ ...inner,
148
+ kind: inner.kind === 'unresolved' ? 'unresolved' : 'alias',
149
+ spec: raw
150
+ };
151
+ }
152
+ // Tarball pin, e.g. `.../@nexartis/sentinel-sdk/-/sentinel-sdk-1.9.2.tgz`.
153
+ const tarballVersion = extractTarballVersion(raw);
154
+ if (tarballVersion !== null) {
155
+ const version = normalizeVersion(tarballVersion);
156
+ if (!version)
157
+ return unresolved(`unparseable tarball version in "${raw}"`);
158
+ return { kind: 'tarball', version, spec: raw, reason: `tarball pin ${version}` };
159
+ }
160
+ if (/^(https?:|file:)/i.test(raw)) {
161
+ return unresolved('tarball without a sentinel-sdk-<version>.tgz name');
162
+ }
163
+ if (raw === '*' || /^(latest|next|x)$/i.test(raw)) {
164
+ return unresolved(`floating specifier "${raw}" — cannot prove a floor`);
165
+ }
166
+ if (raw.includes('||')) {
167
+ return unresolved(`composite range "${raw}" — cannot prove a single floor`);
168
+ }
169
+ const operator = /^(\^|~|>=|<=|>|<|=)/.exec(raw)?.[1] ?? '';
170
+ const versionToken = /(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/.exec(raw);
171
+ if (!versionToken)
172
+ return unresolved(`no semver version in "${raw}"`);
173
+ const version = normalizeVersion(versionToken[0]);
174
+ if (!version)
175
+ return unresolved(`unparseable version in "${raw}"`);
176
+ // `<`/`<=` cannot prove a lower bound at or above the floor.
177
+ if (operator === '<' || operator === '<=') {
178
+ return unresolved(`upper-bounded range "${raw}" — cannot prove a floor`);
179
+ }
180
+ const kind = operator === '' || operator === '=' ? 'exact' : 'range';
181
+ return { kind, version, spec: raw, reason: `${kind} ${operator || '='} ${version}` };
182
+ }
183
+ /**
184
+ * Evaluate one declared specifier against the floor. A missing specifier is
185
+ * `absent` (not a consumer); an unprovable specifier is `unresolved`.
186
+ */
187
+ export function evaluatePin(spec, floor = SENTINEL_SDK_VERSION_FLOOR) {
188
+ if (spec == null || !String(spec).trim()) {
189
+ return {
190
+ status: 'absent',
191
+ pinnedVersion: null,
192
+ spec: spec ?? null,
193
+ reason: `no ${SENTINEL_SDK_PACKAGE} dependency declared`
194
+ };
195
+ }
196
+ const declared = String(spec);
197
+ const parsed = parsePinnedVersion(declared);
198
+ if (parsed.kind === 'unresolved' || !parsed.version) {
199
+ return { status: 'unresolved', pinnedVersion: null, spec: declared, reason: parsed.reason };
200
+ }
201
+ if (compareVersions(parsed.version, floor) < 0) {
202
+ return {
203
+ status: 'below-floor',
204
+ pinnedVersion: parsed.version,
205
+ spec: declared,
206
+ reason: `${parsed.version} resolves below the ${floor} floor`
207
+ };
208
+ }
209
+ return {
210
+ status: 'ok',
211
+ pinnedVersion: parsed.version,
212
+ spec: declared,
213
+ reason: `${parsed.version} is at or above the ${floor} floor`
214
+ };
215
+ }
216
+ /** Evaluate a set of consumers and summarise the fleet verdict. */
217
+ export function evaluateConsumers(entries, floor = SENTINEL_SDK_VERSION_FLOOR) {
218
+ const consumers = entries.map((entry) => ({
219
+ repo: entry.repo,
220
+ ...evaluatePin(entry.dependencySpec, floor)
221
+ }));
222
+ const violations = consumers.filter((c) => c.status === 'below-floor').map((c) => c.repo);
223
+ const unresolved = consumers.filter((c) => c.status === 'unresolved').map((c) => c.repo);
224
+ return {
225
+ floor,
226
+ consumers,
227
+ violations,
228
+ unresolved,
229
+ ok: violations.length === 0,
230
+ strictOk: violations.length === 0 && unresolved.length === 0
231
+ };
232
+ }
233
+ //# sourceMappingURL=consumer-version-floor.js.map