@nexartis/sentinel-sdk 1.9.1 → 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.
- package/README.md +43 -6
- package/dist/core/auth-utils.d.ts.map +1 -1
- package/dist/core/auth-utils.js +23 -3
- package/dist/core/auth-utils.js.map +1 -1
- package/dist/core/client.d.ts +4 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +6 -0
- package/dist/core/client.js.map +1 -1
- package/dist/core/consumer-version-floor.d.ts +56 -0
- package/dist/core/consumer-version-floor.d.ts.map +1 -0
- package/dist/core/consumer-version-floor.js +233 -0
- package/dist/core/consumer-version-floor.js.map +1 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/jwks.d.ts +15 -1
- package/dist/core/jwks.d.ts.map +1 -1
- package/dist/core/jwks.js +54 -10
- package/dist/core/jwks.js.map +1 -1
- package/dist/core/types.d.ts +35 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/sveltekit/cookies.d.ts +10 -0
- package/dist/sveltekit/cookies.d.ts.map +1 -1
- package/dist/sveltekit/cookies.js +13 -0
- package/dist/sveltekit/cookies.js.map +1 -1
- package/dist/sveltekit/handle.js +8 -2
- package/dist/sveltekit/handle.js.map +1 -1
- package/dist/sveltekit/handlers/index.d.ts.map +1 -1
- package/dist/sveltekit/handlers/index.js +30 -5
- package/dist/sveltekit/handlers/index.js.map +1 -1
- package/dist/sveltekit/pages/index.d.ts.map +1 -1
- package/dist/sveltekit/pages/index.js +21 -10
- package/dist/sveltekit/pages/index.js.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @nexartis/sentinel-sdk
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nexartis/sentinel-sdk)
|
|
4
|
-
[](./LICENSE)
|
|
5
5
|
[](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)
|
|
@@ -51,6 +52,22 @@ downstream authorization gates.
|
|
|
51
52
|
refresh to pick up rotated keys.
|
|
52
53
|
- Only `alg: EdDSA` is accepted. `HS256`, `none`, and any other algorithm are
|
|
53
54
|
rejected outright.
|
|
55
|
+
- Since **1.9.2**, signed claims must match the backend's current exact
|
|
56
|
+
`iss: "nexartis"`, `aud: "nexartis"` tuple. Subject, email and JTI must be
|
|
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
|
|
60
|
+
optional `nbf` must be finite nonnegative seconds (including fractional
|
|
61
|
+
seconds) with bounded millisecond conversion. Expired tokens and tokens before `nbf` are rejected.
|
|
62
|
+
- Valid verifier outcomes return only the qualified issuer, audience, subject,
|
|
63
|
+
email, roles (defaulted to `[]` when the token omits the claim),
|
|
64
|
+
`platform_admin`, JTI and NumericDate claims. Unqualified additional claims do not
|
|
65
|
+
acquire tenant, client, membership, or human-presence semantics.
|
|
66
|
+
`DecodedToken` and `decodeTokenLocally` alone are **not** verification evidence.
|
|
67
|
+
- Local JWKS verification **does not query backend JTI revocation**, current
|
|
68
|
+
account status or membership, product/tenant authorization, or proof of humanity.
|
|
69
|
+
The current audience is a service-wide constant, not client-specific isolation.
|
|
70
|
+
Those checks require the appropriate backend/product authority boundary.
|
|
54
71
|
- Verification **never throws**. If the JWKS document is unreachable the
|
|
55
72
|
request fails closed to unauthenticated (not a 500).
|
|
56
73
|
|
|
@@ -72,6 +89,22 @@ release closes.
|
|
|
72
89
|
|
|
73
90
|
---
|
|
74
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
|
+
|
|
75
108
|
## Install
|
|
76
109
|
|
|
77
110
|
```bash
|
|
@@ -101,9 +134,13 @@ The SDK does not bake in any backend URL. Every request is directed at the URL *
|
|
|
101
134
|
|
|
102
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`.
|
|
103
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
|
+
|
|
104
141
|
```bash
|
|
105
142
|
# .env
|
|
106
|
-
SENTINEL_API_URL="https://auth.example.com"
|
|
143
|
+
SENTINEL_API_URL="https://auth.example.com/api"
|
|
107
144
|
PUBLIC_BASE_URL="https://app.example.com"
|
|
108
145
|
```
|
|
109
146
|
|
|
@@ -127,7 +164,7 @@ export const handle = createSentinelHandle({
|
|
|
127
164
|
});
|
|
128
165
|
```
|
|
129
166
|
|
|
130
|
-
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 }`.
|
|
131
168
|
|
|
132
169
|
### 2. Mount the auth API
|
|
133
170
|
|
|
@@ -234,7 +271,7 @@ Either way, this SDK is fully self-service and works against any Sentinel-compat
|
|
|
234
271
|
| Framework-agnostic core | ✅ Use anywhere — Node, Bun, Cloudflare Workers, browsers |
|
|
235
272
|
| Next.js / Remix / Nuxt / SolidStart | Roadmap — core is designed to make new adapters straightforward |
|
|
236
273
|
|
|
237
|
-
Contributions of new adapters are welcome
|
|
274
|
+
Contributions of new adapters are welcome; see [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the contribution terms.
|
|
238
275
|
|
|
239
276
|
---
|
|
240
277
|
|
|
@@ -246,6 +283,6 @@ This SDK follows [Semantic Versioning](https://semver.org/) and a strict **never
|
|
|
246
283
|
|
|
247
284
|
## License
|
|
248
285
|
|
|
249
|
-
Licensed under the
|
|
286
|
+
Licensed under the Apache License, Version 2.0 — see [`LICENSE`](./LICENSE) and [`NOTICE`](./NOTICE). Copyright © Nexartis, LLC. All Rights Reserved.
|
|
250
287
|
|
|
251
|
-
|
|
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;
|
|
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"}
|
package/dist/core/auth-utils.js
CHANGED
|
@@ -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"]}
|
package/dist/core/client.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/core/client.js
CHANGED
|
@@ -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)
|
package/dist/core/client.js.map
CHANGED
|
@@ -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
|