@hearth-auth/sdk 0.0.1 → 1.0.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 (83) hide show
  1. package/dist/admin.d.ts +43 -0
  2. package/dist/admin.js +126 -0
  3. package/dist/admin.js.map +1 -0
  4. package/dist/browser-auth.d.ts +32 -0
  5. package/dist/browser-auth.js +99 -0
  6. package/dist/browser-auth.js.map +1 -0
  7. package/dist/claims.d.ts +86 -0
  8. package/dist/claims.js +137 -0
  9. package/dist/claims.js.map +1 -0
  10. package/dist/client.d.ts +77 -0
  11. package/dist/client.js +190 -0
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors.d.ts +114 -0
  14. package/{src/errors.ts → dist/errors.js} +83 -97
  15. package/dist/errors.js.map +1 -0
  16. package/dist/hearth-client.d.ts +133 -0
  17. package/dist/hearth-client.js +192 -0
  18. package/dist/hearth-client.js.map +1 -0
  19. package/dist/hearth.d.ts +105 -0
  20. package/dist/hearth.js +109 -0
  21. package/dist/hearth.js.map +1 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.js +22 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/introspection-client.d.ts +59 -0
  26. package/dist/introspection-client.js +36 -0
  27. package/dist/introspection-client.js.map +1 -0
  28. package/dist/jwks-client.d.ts +28 -0
  29. package/dist/jwks-client.js +28 -0
  30. package/dist/jwks-client.js.map +1 -0
  31. package/dist/middleware.d.ts +38 -0
  32. package/dist/middleware.js +51 -0
  33. package/dist/middleware.js.map +1 -0
  34. package/dist/pkce.d.ts +64 -0
  35. package/dist/pkce.js +64 -0
  36. package/dist/pkce.js.map +1 -0
  37. package/dist/react.d.ts +32 -0
  38. package/dist/react.js +41 -0
  39. package/dist/react.js.map +1 -0
  40. package/dist/session-version-cache.d.ts +50 -0
  41. package/dist/session-version-cache.js +129 -0
  42. package/dist/session-version-cache.js.map +1 -0
  43. package/dist/types.d.ts +168 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +13 -4
  47. package/CHANGELOG.md +0 -12
  48. package/src/admin.ts +0 -157
  49. package/src/browser-auth.ts +0 -130
  50. package/src/claims.ts +0 -180
  51. package/src/client.ts +0 -251
  52. package/src/generated/google/api/annotations_pb.ts +0 -44
  53. package/src/generated/google/api/http_pb.ts +0 -467
  54. package/src/generated/hearth/authz/v1/authz_pb.ts +0 -593
  55. package/src/generated/hearth/cluster/v1/raft_pb.ts +0 -183
  56. package/src/generated/hearth/events/v1/audit_pb.ts +0 -886
  57. package/src/generated/hearth/identity/v1/identity_pb.ts +0 -1673
  58. package/src/generated/hearth/identity/v1/oauth_pb.ts +0 -1138
  59. package/src/generated/hearth/rbac/v1/rbac_pb.ts +0 -2000
  60. package/src/hearth-client.ts +0 -288
  61. package/src/hearth.ts +0 -224
  62. package/src/index.ts +0 -106
  63. package/src/introspection-client.ts +0 -83
  64. package/src/jwks-client.ts +0 -45
  65. package/src/middleware.ts +0 -82
  66. package/src/pkce.ts +0 -129
  67. package/src/react.tsx +0 -57
  68. package/src/session-version-cache.ts +0 -167
  69. package/src/types.ts +0 -188
  70. package/tests/admin-crud.test.ts +0 -97
  71. package/tests/auth-flow.test.ts +0 -75
  72. package/tests/authorize.test.ts +0 -386
  73. package/tests/claims.test.ts +0 -159
  74. package/tests/hasPermission.test.ts +0 -152
  75. package/tests/hearth-client.test.ts +0 -243
  76. package/tests/helpers.ts +0 -90
  77. package/tests/jwks.test.ts +0 -62
  78. package/tests/pkce.test.ts +0 -210
  79. package/tests/react-useHasPermission.test.tsx +0 -92
  80. package/tests/required-action.test.ts +0 -276
  81. package/tests/session-version.test.ts +0 -391
  82. package/tsconfig.json +0 -16
  83. package/vitest.config.ts +0 -8
@@ -0,0 +1,168 @@
1
+ /** Response from the dev bootstrap endpoint. */
2
+ export interface BootstrapResponse {
3
+ realm_id: string;
4
+ user_id: string;
5
+ access_token: string;
6
+ refresh_token: string;
7
+ }
8
+ /** Parameters for initiating an authorization code flow. */
9
+ export interface AuthorizeParams {
10
+ clientId: string;
11
+ redirectUri: string;
12
+ scope: string;
13
+ state: string;
14
+ responseType?: string;
15
+ userId: string;
16
+ codeChallenge?: string;
17
+ codeChallengeMethod?: string;
18
+ nonce?: string;
19
+ }
20
+ /** Response from the authorize endpoint. */
21
+ export interface AuthorizeResponse {
22
+ code: string;
23
+ state: string;
24
+ }
25
+ /** Parameters for exchanging an authorization code. */
26
+ export interface TokenExchangeParams {
27
+ clientId: string;
28
+ code: string;
29
+ redirectUri: string;
30
+ codeVerifier?: string;
31
+ }
32
+ /** Response from the token exchange endpoint. */
33
+ export interface TokenResponse {
34
+ access_token: string;
35
+ id_token: string;
36
+ token_type: string;
37
+ expires_in: number;
38
+ refresh_token: string;
39
+ }
40
+ /** UserInfo response from the OIDC UserInfo endpoint. */
41
+ export interface UserInfoResponse {
42
+ sub: string;
43
+ name?: string;
44
+ email?: string;
45
+ email_verified?: boolean;
46
+ }
47
+ /** Parameters for creating a user. */
48
+ export interface CreateUserParams {
49
+ email: string;
50
+ displayName: string;
51
+ }
52
+ /** User record from the API. */
53
+ export interface User {
54
+ id: string;
55
+ email: string;
56
+ display_name: string;
57
+ status: string;
58
+ created_at?: number;
59
+ updated_at?: number;
60
+ }
61
+ /** Parameters for updating a user. */
62
+ export interface UpdateUserParams {
63
+ email?: string;
64
+ displayName?: string;
65
+ status?: string;
66
+ }
67
+ /** Parameters for creating a realm. */
68
+ export interface CreateRealmParams {
69
+ name: string;
70
+ config?: Record<string, unknown>;
71
+ }
72
+ /** Realm record from the API. */
73
+ export interface Realm {
74
+ id: string;
75
+ name: string;
76
+ status: string;
77
+ config: Record<string, unknown> | null;
78
+ created_at?: number;
79
+ updated_at?: number;
80
+ }
81
+ /** Parameters for updating a realm. */
82
+ export interface UpdateRealmParams {
83
+ name?: string;
84
+ status?: string;
85
+ config?: Record<string, unknown>;
86
+ }
87
+ /** Paginated list response. */
88
+ export interface PageResponse<T> {
89
+ items: T[];
90
+ next_cursor: string | null;
91
+ }
92
+ /** Parameters for registering an OAuth client. */
93
+ export interface RegisterClientParams {
94
+ clientName: string;
95
+ redirectUris: string[];
96
+ }
97
+ /** OAuth client record from the API. */
98
+ export interface OAuthClient {
99
+ client_id: string;
100
+ client_name: string;
101
+ redirect_uris: string[];
102
+ grant_types: string[];
103
+ created_at?: number;
104
+ }
105
+ /** JWKS document containing public keys. */
106
+ export interface JwksDocument {
107
+ keys: JsonWebKey[];
108
+ }
109
+ /** A single JWK entry. */
110
+ export interface JsonWebKey {
111
+ kty: string;
112
+ crv?: string;
113
+ x?: string;
114
+ kid?: string;
115
+ use?: string;
116
+ alg?: string;
117
+ }
118
+ /**
119
+ * Response from `GET /v1/me/permissions`.
120
+ *
121
+ * Returns the freshly-resolved RBAC claim set for the bearer-token user.
122
+ */
123
+ export interface MePermissionsResponse {
124
+ roles: string[];
125
+ groups: string[];
126
+ permissions: string[];
127
+ scope: string;
128
+ }
129
+ /** The three permission delivery modes introduced in HEA-922. */
130
+ export type AccessTokenAuthorizationMode = "embedded" | "introspection" | "decision";
131
+ /** Options for a per-request permission decision call to `POST /oauth/authorize`. */
132
+ export interface AuthorizePermissionOptions {
133
+ /** Constrain the decision to a specific organization. */
134
+ organizationId?: string;
135
+ /** Constrain the decision to a specific resource. */
136
+ resource?: string;
137
+ }
138
+ /**
139
+ * Configuration for the client-side session-version cache (RFC HEA-930 § 13).
140
+ *
141
+ * When enabled, the SDK fetches a snapshot of `{sessionId → minSv}` on startup,
142
+ * polls `GET /oauth/session-versions` for deltas at `pollIntervalMs` intervals,
143
+ * and validates the `sv` claim on every `hasPermission` / `hasRole` / `inGroup`
144
+ * / `inOrg` call without any per-request network hop.
145
+ */
146
+ export interface SessionVersionConfig {
147
+ /** Whether session-version validation is enabled. */
148
+ enabled: boolean;
149
+ /** Delta feed poll interval in milliseconds. Recommended: 5 000. */
150
+ pollIntervalMs: number;
151
+ /**
152
+ * Maximum cache age before the cache is considered stale, in milliseconds.
153
+ * MUST be greater than `pollIntervalMs`. Recommended: `pollIntervalMs × 3`.
154
+ */
155
+ staleThresholdMs: number;
156
+ /**
157
+ * Action when the cache exceeds `staleThresholdMs`:
158
+ * - `"reject"` — throw {@link SessionVersionCacheStaleError} (fail-closed).
159
+ * - `"introspect"` — caller should catch {@link SessionVersionCacheStaleError}
160
+ * and fall back to the introspection endpoint.
161
+ */
162
+ onStale: "reject" | "introspect";
163
+ /**
164
+ * Service-to-service access token with `hearth.sv_feed` scope.
165
+ * Required when `enabled` is `true`.
166
+ */
167
+ serviceToken: string;
168
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,14 +1,23 @@
1
1
  {
2
2
  "name": "@hearth-auth/sdk",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/hearth-auth/hearth.git",
7
+ "directory": "sdks/typescript"
8
+ },
4
9
  "type": "module",
5
- "types": "./src/index.ts",
10
+ "types": "./dist/index.d.ts",
6
11
  "exports": {
7
12
  ".": {
8
- "types": "./src/index.ts",
9
- "default": "./src/index.ts"
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
10
16
  }
11
17
  },
18
+ "files": [
19
+ "dist"
20
+ ],
12
21
  "scripts": {
13
22
  "build": "tsc",
14
23
  "test": "vitest run",
package/CHANGELOG.md DELETED
@@ -1,12 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to `@hearth-auth/browser` and `@hearth-auth/node` are documented here.
4
-
5
- ## [Unreleased]
6
-
7
- ### Changed
8
- - SDK brought into conformance with the [Hearth SDK Common Specification](../../docs/specs/SDK.md).
9
- - All 9 required error types from spec §5 are now exported.
10
- - Full Claims API (spec §4) implemented on verified token objects.
11
- - JWKS caching follows the 5-rule contract from spec §2.
12
- - README updated with installation, quickstart, and troubleshooting sections (spec §10).
package/src/admin.ts DELETED
@@ -1,157 +0,0 @@
1
- import { HearthError } from "./client.js";
2
- import type {
3
- CreateRealmParams,
4
- CreateUserParams,
5
- PageResponse,
6
- Realm,
7
- UpdateRealmParams,
8
- UpdateUserParams,
9
- User,
10
- } from "./types.js";
11
-
12
- /**
13
- * Admin API client for Hearth.
14
- *
15
- * Requires a valid admin access token. All operations go through
16
- * the /admin/* endpoints which enforce RBAC admin role checks.
17
- */
18
- export class AdminClient {
19
- constructor(
20
- private readonly baseUrl: string,
21
- private readonly realmId: string,
22
- private readonly accessToken: string,
23
- ) {}
24
-
25
- // === Users ===
26
-
27
- /** POST /admin/users — create a user. */
28
- async createUser(params: CreateUserParams): Promise<User> {
29
- return this.post("/admin/users", {
30
- email: params.email,
31
- display_name: params.displayName,
32
- });
33
- }
34
-
35
- /** GET /admin/users — list users with pagination. */
36
- async listUsers(options?: {
37
- limit?: number;
38
- cursor?: string;
39
- }): Promise<PageResponse<User>> {
40
- const q = new URLSearchParams();
41
- if (options?.limit) q.set("limit", String(options.limit));
42
- if (options?.cursor) q.set("cursor", options.cursor);
43
- return this.get(`/admin/users?${q}`);
44
- }
45
-
46
- /** GET /admin/users/:id — get a user by ID. */
47
- async getUser(userId: string): Promise<User> {
48
- return this.get(`/admin/users/${userId}`);
49
- }
50
-
51
- /** PUT /admin/users/:id — update a user. */
52
- async updateUser(userId: string, params: UpdateUserParams): Promise<User> {
53
- return this.request("PATCH", `/admin/users/${userId}`, {
54
- email: params.email,
55
- display_name: params.displayName,
56
- status: params.status,
57
- });
58
- }
59
-
60
- /** DELETE /admin/users/:id — delete a user. */
61
- async deleteUser(userId: string): Promise<void> {
62
- const resp = await fetch(`${this.baseUrl}/admin/users/${userId}`, {
63
- method: "DELETE",
64
- headers: this.headers(),
65
- });
66
- if (!resp.ok) {
67
- throw new HearthError(resp.status, await resp.json());
68
- }
69
- }
70
-
71
- // === Realms ===
72
-
73
- /** POST /admin/realms — create a realm. */
74
- async createRealm(params: CreateRealmParams): Promise<Realm> {
75
- return this.post("/admin/realms", {
76
- name: params.name,
77
- config: params.config,
78
- });
79
- }
80
-
81
- /** GET /admin/realms — list realms with pagination. */
82
- async listRealms(options?: {
83
- limit?: number;
84
- cursor?: string;
85
- }): Promise<PageResponse<Realm>> {
86
- const q = new URLSearchParams();
87
- if (options?.limit) q.set("limit", String(options.limit));
88
- if (options?.cursor) q.set("cursor", options.cursor);
89
- return this.get(`/admin/realms?${q}`);
90
- }
91
-
92
- /** GET /admin/realms/:id — get a realm by ID. */
93
- async getRealm(realmId: string): Promise<Realm> {
94
- return this.get(`/admin/realms/${realmId}`);
95
- }
96
-
97
- /** PUT /admin/realms/:id — update a realm. */
98
- async updateRealm(
99
- realmId: string,
100
- params: UpdateRealmParams,
101
- ): Promise<Realm> {
102
- return this.request("PATCH", `/admin/realms/${realmId}`, {
103
- name: params.name,
104
- status: params.status,
105
- config: params.config,
106
- });
107
- }
108
-
109
- /** DELETE /admin/realms/:id — delete a realm. */
110
- async deleteRealm(realmId: string): Promise<void> {
111
- const resp = await fetch(`${this.baseUrl}/admin/realms/${realmId}`, {
112
- method: "DELETE",
113
- headers: this.headers(),
114
- });
115
- if (!resp.ok) {
116
- throw new HearthError(resp.status, await resp.json());
117
- }
118
- }
119
-
120
- private headers(): Record<string, string> {
121
- return {
122
- "X-Realm-ID": this.realmId,
123
- Authorization: `Bearer ${this.accessToken}`,
124
- "Content-Type": "application/json",
125
- };
126
- }
127
-
128
- private async get<T>(path: string): Promise<T> {
129
- const resp = await fetch(`${this.baseUrl}${path}`, {
130
- headers: this.headers(),
131
- });
132
- if (!resp.ok) {
133
- throw new HearthError(resp.status, await resp.json());
134
- }
135
- return resp.json() as Promise<T>;
136
- }
137
-
138
- private async post<T>(path: string, body: unknown): Promise<T> {
139
- return this.request("POST", path, body);
140
- }
141
-
142
- private async request<T>(
143
- method: string,
144
- path: string,
145
- body: unknown,
146
- ): Promise<T> {
147
- const resp = await fetch(`${this.baseUrl}${path}`, {
148
- method,
149
- headers: this.headers(),
150
- body: JSON.stringify(body),
151
- });
152
- if (!resp.ok) {
153
- throw new HearthError(resp.status, await resp.json());
154
- }
155
- return resp.json() as Promise<T>;
156
- }
157
- }
@@ -1,130 +0,0 @@
1
- import { HearthApiClient } from "./client.js";
2
- import { startLogin } from "./pkce.js";
3
- import type { TokenResponse } from "./types.js";
4
-
5
- // ── Token store ─────────────────────────────────────────────────────────────
6
- // Access token lives in memory only. Refresh + ID tokens survive page reloads
7
- // via localStorage. For stricter XSS safety, swap for an HttpOnly-cookie BFF.
8
-
9
- const REFRESH_KEY = "hearth_refresh_token";
10
- const ID_KEY = "hearth_id_token";
11
-
12
- let _accessToken: string | null = null;
13
- let _expiresAt: number | null = null;
14
- let _refreshTimer: ReturnType<typeof setTimeout> | null = null;
15
-
16
- export function getAccessToken(): string | null { return _accessToken; }
17
- export function getRefreshToken(): string | null { return localStorage.getItem(REFRESH_KEY); }
18
- export function getIdToken(): string | null { return localStorage.getItem(ID_KEY); }
19
-
20
- /** True iff an access token is present and not yet expired. */
21
- export function isAuthenticated(): boolean {
22
- return _accessToken !== null && _expiresAt !== null && Date.now() / 1000 < _expiresAt;
23
- }
24
-
25
- export function clearTokens(): void {
26
- _accessToken = null;
27
- _expiresAt = null;
28
- localStorage.removeItem(REFRESH_KEY);
29
- localStorage.removeItem(ID_KEY);
30
- if (_refreshTimer !== null) { clearTimeout(_refreshTimer); _refreshTimer = null; }
31
- }
32
-
33
- function storeTokens(tokens: TokenResponse, fallbackRefresh?: string): void {
34
- _accessToken = tokens.access_token;
35
- _expiresAt = Date.now() / 1000 + (tokens.expires_in ?? 3600);
36
- const rt = tokens.refresh_token ?? fallbackRefresh;
37
- if (rt) localStorage.setItem(REFRESH_KEY, rt);
38
- if (tokens.id_token) localStorage.setItem(ID_KEY, tokens.id_token);
39
- }
40
-
41
- function scheduleRefresh(expiresIn: number, doRefresh: () => Promise<void>): void {
42
- if (_refreshTimer !== null) clearTimeout(_refreshTimer);
43
- const delayMs = Math.max(expiresIn * 0.8, expiresIn - 60) * 1000;
44
- _refreshTimer = setTimeout(() => { void doRefresh().catch(() => { /* re-auth on next action */ }); }, delayMs);
45
- }
46
-
47
- // ── Auth config ──────────────────────────────────────────────────────────────
48
-
49
- /** Configuration for {@link createHearthAuth}. */
50
- export interface AuthConfig {
51
- /** OAuth 2.0 client ID. */
52
- clientId: string;
53
- /** Redirect URI registered for this client. */
54
- redirectUri: string;
55
- /** Hearth server base URL, e.g. `http://localhost:8420`. */
56
- hearthUrl: string;
57
- /** Realm name (slug), e.g. `"demo"`. */
58
- realmSlug: string;
59
- }
60
-
61
- /** Auth facade returned by {@link createHearthAuth}. */
62
- export interface HearthBrowserAuth {
63
- startLogin(): Promise<void>;
64
- handleCallback(code: string, state: string): Promise<void>;
65
- refreshAccessToken(): Promise<void>;
66
- logout(): Promise<void>;
67
- }
68
-
69
- const VERIFIER_KEY = "hearth_pkce_verifier";
70
- const STATE_KEY = "hearth_oauth_state";
71
-
72
- /**
73
- * Create a browser-side Hearth auth facade backed entirely by the SDK.
74
- *
75
- * Handles the full PKCE login flow, token storage, silent refresh, and
76
- * RP-initiated logout. No custom crypto or OIDC endpoint logic required.
77
- */
78
- export function createHearthAuth(
79
- client: HearthApiClient,
80
- config: AuthConfig,
81
- ): HearthBrowserAuth {
82
- async function refreshAccessToken(): Promise<void> {
83
- const rt = getRefreshToken();
84
- if (!rt) throw new Error("No refresh token stored");
85
- const tokens = await client.refreshTokens(config.clientId, rt);
86
- storeTokens(tokens, rt);
87
- scheduleRefresh(tokens.expires_in ?? 3600, refreshAccessToken);
88
- }
89
-
90
- return {
91
- async startLogin(): Promise<void> {
92
- const { url, state, codeVerifier } = await startLogin(client, {
93
- clientId: config.clientId,
94
- redirectUri: config.redirectUri,
95
- });
96
- sessionStorage.setItem(VERIFIER_KEY, codeVerifier);
97
- sessionStorage.setItem(STATE_KEY, state);
98
- window.location.href = url;
99
- },
100
-
101
- async handleCallback(_code: string, state: string): Promise<void> {
102
- const storedState = sessionStorage.getItem(STATE_KEY);
103
- const codeVerifier = sessionStorage.getItem(VERIFIER_KEY) ?? undefined;
104
- sessionStorage.removeItem(STATE_KEY);
105
- sessionStorage.removeItem(VERIFIER_KEY);
106
- if (storedState !== state) throw new Error("State mismatch — possible CSRF");
107
- const tokens = await client.handleCallback({
108
- callbackUrl: window.location.href,
109
- clientId: config.clientId,
110
- redirectUri: config.redirectUri,
111
- codeVerifier,
112
- });
113
- storeTokens(tokens);
114
- scheduleRefresh(tokens.expires_in ?? 3600, refreshAccessToken);
115
- },
116
-
117
- refreshAccessToken,
118
-
119
- async logout(): Promise<void> {
120
- const idToken = getIdToken();
121
- clearTokens();
122
- const doc = await client.discovery().catch(() => null);
123
- const end = (doc?.["end_session_endpoint"] as string | undefined)
124
- ?? `${config.hearthUrl}/realms/${config.realmSlug}/end_session`;
125
- const params = new URLSearchParams({ post_logout_redirect_uri: window.location.origin });
126
- if (idToken) params.set("id_token_hint", idToken);
127
- window.location.href = `${end}?${params}`;
128
- },
129
- };
130
- }
package/src/claims.ts DELETED
@@ -1,180 +0,0 @@
1
- /**
2
- * Spec §4 — Claims API.
3
- *
4
- * {@link Claims} wraps a decoded JWT payload and exposes typed accessors
5
- * for standard OIDC and Hearth-specific claims. All reads are local —
6
- * no network call is made. Signature verification is the caller's
7
- * responsibility (e.g. via the JWKS endpoint before constructing Claims).
8
- */
9
-
10
- import { decodeJwt } from "jose";
11
- import {
12
- TokenExpiredError,
13
- TokenInvalidError,
14
- TokenNotYetValidError,
15
- } from "./errors.js";
16
-
17
- /** Raw JWT payload shape used internally. */
18
- interface RawPayload {
19
- sub?: string;
20
- iss?: string;
21
- aud?: string | string[];
22
- exp?: number;
23
- nbf?: number;
24
- iat?: number;
25
- jti?: string;
26
- scope?: string;
27
- scopes?: string[];
28
- roles?: string[];
29
- permissions?: string[];
30
- groups?: string[];
31
- oid?: string;
32
- org_groups?: string[];
33
- token_type?: string;
34
- [key: string]: unknown;
35
- }
36
-
37
- /**
38
- * Typed accessor for a decoded JWT's claims.
39
- *
40
- * Construct via {@link Claims.decode} (decodes without verifying signature)
41
- * or pass a pre-decoded payload to the constructor.
42
- */
43
- export class Claims {
44
- private readonly payload: RawPayload;
45
-
46
- constructor(payload: RawPayload) {
47
- this.payload = payload;
48
- }
49
-
50
- /**
51
- * Decode a JWT string into a {@link Claims} object.
52
- * The signature is NOT verified — the caller must verify it separately.
53
- *
54
- * @throws {TokenInvalidError} if the string is not a valid JWT.
55
- */
56
- static decode(token: string): Claims {
57
- try {
58
- const payload = decodeJwt(token) as RawPayload;
59
- return new Claims(payload);
60
- } catch (err) {
61
- throw new TokenInvalidError(
62
- `Failed to decode JWT: ${err instanceof Error ? err.message : String(err)}`,
63
- );
64
- }
65
- }
66
-
67
- /**
68
- * Assert the token is temporally valid (not expired, past nbf).
69
- *
70
- * @throws {TokenExpiredError} if exp is in the past.
71
- * @throws {TokenNotYetValidError} if nbf is in the future.
72
- */
73
- assertValid(clockSkewSeconds = 0): void {
74
- const now = Math.floor(Date.now() / 1000);
75
- const exp = this.payload.exp;
76
- if (exp !== undefined && now > exp + clockSkewSeconds) {
77
- throw new TokenExpiredError(new Date(exp * 1000));
78
- }
79
- const nbf = this.payload.nbf;
80
- if (nbf !== undefined && now < nbf - clockSkewSeconds) {
81
- throw new TokenNotYetValidError(new Date(nbf * 1000));
82
- }
83
- }
84
-
85
- /** The `sub` (subject) claim — identifies the principal that is the subject of the JWT. */
86
- subject(): string {
87
- return this.payload.sub ?? "";
88
- }
89
-
90
- /** The `iss` (issuer) claim — identifies the principal that issued the JWT. */
91
- issuer(): string {
92
- return this.payload.iss ?? "";
93
- }
94
-
95
- /** The `aud` (audiences) claim — normalized to an array. */
96
- audiences(): string[] {
97
- const aud = this.payload.aud;
98
- if (!aud) return [];
99
- return Array.isArray(aud) ? aud : [aud];
100
- }
101
-
102
- /** The `exp` (expiry) claim as a Date, or null if absent. */
103
- expiry(): Date | null {
104
- return this.payload.exp !== undefined
105
- ? new Date(this.payload.exp * 1000)
106
- : null;
107
- }
108
-
109
- /** The `iat` (issuedAt) claim as a Date, or null if absent. */
110
- issuedAt(): Date | null {
111
- return this.payload.iat !== undefined
112
- ? new Date(this.payload.iat * 1000)
113
- : null;
114
- }
115
-
116
- /** The `jti` (JWT ID) claim, or null if absent. */
117
- jwtID(): string | null {
118
- return this.payload.jti ?? null;
119
- }
120
-
121
- /** The `scope` claim split into individual scopes (or `scopes` array if present). */
122
- scopes(): string[] {
123
- if (this.payload.scopes) return this.payload.scopes;
124
- const scope = this.payload.scope;
125
- if (!scope) return [];
126
- return scope.split(/\s+/).filter(Boolean);
127
- }
128
-
129
- /** Returns true iff the token contains the given scope. */
130
- hasScope(scope: string): boolean {
131
- return this.scopes().includes(scope);
132
- }
133
-
134
- /** Returns true iff the token's `roles` claim contains the given role. */
135
- hasRole(role: string): boolean {
136
- return (this.payload.roles ?? []).includes(role);
137
- }
138
-
139
- /** Returns true iff the token's `permissions` claim contains the given permission. */
140
- hasPermission(permission: string): boolean {
141
- return (this.payload.permissions ?? []).includes(permission);
142
- }
143
-
144
- /** The raw `scope` claim (space-delimited string), or empty string if absent. */
145
- scope(): string {
146
- return this.payload.scope ?? "";
147
- }
148
-
149
- /** Returns true iff the token's `groups` claim contains the given group. */
150
- inGroup(groupId: string): boolean {
151
- const groups = this.payload.groups;
152
- return Array.isArray(groups) && groups.includes(groupId);
153
- }
154
-
155
- /** Returns true iff the token's `oid` claim exactly matches the given org ID. */
156
- inOrg(orgId: string): boolean {
157
- return typeof this.payload.oid === "string" && this.payload.oid === orgId;
158
- }
159
-
160
- /** The `token_type` claim (`"access"`, `"refresh"`, `"required_action"`), or empty string. */
161
- tokenType(): string {
162
- return this.payload.token_type ?? "";
163
- }
164
-
165
- /** The `oid` (organization ID) claim, or `undefined` if absent. */
166
- organizationId(): string | undefined {
167
- return this.payload.oid;
168
- }
169
-
170
- /** The `org_groups` claim (Keycloak-style org-scoped group paths), or empty array. */
171
- orgGroups(): string[] {
172
- const og = this.payload.org_groups;
173
- return Array.isArray(og) ? og : [];
174
- }
175
-
176
- /** Access an arbitrary claim by key. */
177
- get(key: string): unknown {
178
- return this.payload[key];
179
- }
180
- }