@hearth-auth/node 1.0.19 → 1.1.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 +320 -0
- package/dist/client.d.ts +86 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +108 -2
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +83 -0
- package/dist/client.test.js.map +1 -1
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js.map +1 -1
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -1
- package/dist/flows.d.ts +181 -0
- package/dist/flows.d.ts.map +1 -0
- package/dist/flows.js +332 -0
- package/dist/flows.js.map +1 -0
- package/dist/flows.test.d.ts +3 -0
- package/dist/flows.test.d.ts.map +1 -0
- package/dist/flows.test.js +332 -0
- package/dist/flows.test.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/pkce.d.ts +29 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +18 -0
- package/dist/pkce.js.map +1 -0
- package/dist/pkce.test.d.ts +3 -0
- package/dist/pkce.test.d.ts.map +1 -0
- package/dist/pkce.test.js +46 -0
- package/dist/pkce.test.js.map +1 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Hearth Node.js SDK
|
|
2
|
+
|
|
3
|
+
> **SDK Specification:** This SDK must conform to the [Hearth SDK Common Specification](../../docs/specs/SDK.md).
|
|
4
|
+
|
|
5
|
+
Server-side Node.js client for [Hearth](https://github.com/hearth-auth/hearth). Covers JWT verification, token introspection, Express/Fastify middleware, and the Admin API.
|
|
6
|
+
|
|
7
|
+
**Use this SDK when:** you are building a Node.js server that must verify Hearth-issued tokens or enforce permission checks on incoming requests.
|
|
8
|
+
|
|
9
|
+
**Use [`@hearth-auth/sdk`](../typescript/README.md) instead when:** you need a browser/React integration, PKCE authorization-code flow, or React hooks (`useHasPermission`, `HearthProvider`).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @hearth-auth/node
|
|
17
|
+
# or
|
|
18
|
+
yarn add @hearth-auth/node
|
|
19
|
+
# or
|
|
20
|
+
pnpm add @hearth-auth/node
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| SDK version | Minimum Node.js | Minimum Hearth server |
|
|
24
|
+
|-------------|-----------------|----------------------|
|
|
25
|
+
| 1.0.x | 18.0.0 | 1.0.0 |
|
|
26
|
+
|
|
27
|
+
**Peer dependencies:** none. The SDK ships `jose` as a direct dependency for JWKS verification.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { HearthClient } from "@hearth-auth/node";
|
|
35
|
+
|
|
36
|
+
const client = new HearthClient({
|
|
37
|
+
issuer_url: "https://hearth.example.com",
|
|
38
|
+
client_id: process.env.HEARTH_CLIENT_ID, // required for audience validation
|
|
39
|
+
client_secret: process.env.HEARTH_CLIENT_SECRET, // required for introspection/Decision mode
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Verify an incoming Bearer token (JWKS-based, local, no network on cache hit)
|
|
43
|
+
const token = await client.verifyToken(rawToken);
|
|
44
|
+
// use your logger — avoid logging sub/email to stdout (PII in container logs)
|
|
45
|
+
console.log("Has permission:", token.hasPermission("docs.write"));
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`HearthClient` auto-discovers all endpoint URLs from `{issuer_url}/.well-known/openid-configuration` on first use.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Token verification
|
|
53
|
+
|
|
54
|
+
> **Audience validation:** always supply `client_id`. Without it the SDK has no audience to
|
|
55
|
+
> compare the JWT `aud` claim against, leaving the server open to token-confusion attacks
|
|
56
|
+
> (RFC 7519 §4.1.3). Omit `client_id` only when this server intentionally accepts tokens
|
|
57
|
+
> issued for any client (e.g. a pure gateway that delegates authz downstream).
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { HearthClient, TokenExpiredError, TokenInvalidError } from "@hearth-auth/node";
|
|
61
|
+
|
|
62
|
+
const client = new HearthClient({
|
|
63
|
+
issuer_url: "https://hearth.example.com",
|
|
64
|
+
client_id: process.env.HEARTH_CLIENT_ID, // enables JWT `aud` validation
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const token = await client.verifyToken(rawToken);
|
|
69
|
+
// token is a VerifiedToken — typed access to all claims
|
|
70
|
+
token.subject(); // JWT `sub`
|
|
71
|
+
token.issuer(); // JWT `iss`
|
|
72
|
+
token.expiry(); // Date | null
|
|
73
|
+
token.hasRole("admin"); // reads `roles` claim
|
|
74
|
+
token.hasPermission("docs.write"); // reads `permissions` claim
|
|
75
|
+
token.inGroup("engineering"); // reads `groups` claim
|
|
76
|
+
token.inOrg("org_acme"); // reads `oid` claim
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err instanceof TokenExpiredError) {
|
|
79
|
+
// 401 — ask client to refresh
|
|
80
|
+
} else if (err instanceof TokenInvalidError) {
|
|
81
|
+
// 401 — reject the request
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
JWKS keys are cached by `kid`. On a cache miss the SDK re-fetches the JWKS once before
|
|
87
|
+
failing — this handles transparent key rotation. Call `client.invalidateCache()` after
|
|
88
|
+
receiving a `401` from a downstream service to force a re-fetch.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Express middleware
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import express from "express";
|
|
96
|
+
import { hearthMiddleware } from "@hearth-auth/node";
|
|
97
|
+
|
|
98
|
+
const app = express();
|
|
99
|
+
|
|
100
|
+
// Protect all routes — embedded mode (JWKS only, no network per request)
|
|
101
|
+
app.use(
|
|
102
|
+
hearthMiddleware({
|
|
103
|
+
issuer_url: "https://hearth.example.com",
|
|
104
|
+
expectedMode: "embedded",
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Access the verified token downstream via req.hearthToken
|
|
109
|
+
app.get("/me", (req, res) => {
|
|
110
|
+
res.json({ sub: req.hearthToken?.subject() });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Require a specific permission on a single route
|
|
114
|
+
app.post(
|
|
115
|
+
"/docs",
|
|
116
|
+
hearthMiddleware({
|
|
117
|
+
issuer_url: "https://hearth.example.com",
|
|
118
|
+
expectedMode: "embedded",
|
|
119
|
+
requiredPermission: "docs.write",
|
|
120
|
+
}),
|
|
121
|
+
docsHandler
|
|
122
|
+
);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The middleware responds `401 Unauthorized` with `WWW-Authenticate: Bearer realm="hearth"` on
|
|
126
|
+
missing or invalid tokens, and `403 Forbidden` on scope/role/permission failures. It never
|
|
127
|
+
calls `next` on auth failure.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Fastify hook
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import Fastify from "fastify";
|
|
135
|
+
import { hearthFastifyHook } from "@hearth-auth/node";
|
|
136
|
+
|
|
137
|
+
const app = Fastify();
|
|
138
|
+
|
|
139
|
+
app.addHook(
|
|
140
|
+
"onRequest",
|
|
141
|
+
hearthFastifyHook({
|
|
142
|
+
issuer_url: "https://hearth.example.com",
|
|
143
|
+
expectedMode: "embedded",
|
|
144
|
+
requiredRole: "admin",
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The hook calls `reply.code(401).send(...)` on missing or invalid tokens and
|
|
150
|
+
`reply.code(403).send(...)` on permission failures. It always calls `reply.hijack()` on
|
|
151
|
+
failure so no downstream handler runs. It never resolves to `next` on auth failure.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Permission delivery modes
|
|
156
|
+
|
|
157
|
+
Hearth supports three modes for how RBAC data reaches your resource server. The mode must
|
|
158
|
+
match the `access_token_authorization` setting on the registered OAuth client.
|
|
159
|
+
|
|
160
|
+
| Mode | Strategy | Network per request |
|
|
161
|
+
|------|----------|-------------------|
|
|
162
|
+
| `embedded` (default) | RBAC claims baked into JWT at issuance; verify via JWKS | None |
|
|
163
|
+
| `introspection` | Call `POST /introspect`; server re-resolves live RBAC | 1 |
|
|
164
|
+
| `decision` | Call `POST /oauth/authorize`; server returns `allowed` | 1 |
|
|
165
|
+
|
|
166
|
+
> **Design constraint:** the SDK never infers mode from whether `permissions` is present in
|
|
167
|
+
> the token. Declare `expectedMode` explicitly; absence of `permissions` in `embedded` mode
|
|
168
|
+
> means the user has no permissions, not that a different mode should be tried.
|
|
169
|
+
|
|
170
|
+
### Introspection mode
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { HearthClient } from "@hearth-auth/node";
|
|
174
|
+
|
|
175
|
+
const client = new HearthClient({
|
|
176
|
+
issuer_url: "https://hearth.example.com",
|
|
177
|
+
client_id: "<resource-server-client-id>",
|
|
178
|
+
client_secret: process.env.RS_SECRET,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await client.introspect(rawToken);
|
|
182
|
+
if (result.active) {
|
|
183
|
+
console.log("Live permissions:", result.extra?.permissions);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Or via middleware:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
app.use(
|
|
191
|
+
hearthMiddleware({
|
|
192
|
+
issuer_url: "https://hearth.example.com",
|
|
193
|
+
client_id: "<resource-server-client-id>",
|
|
194
|
+
client_secret: process.env.RS_SECRET,
|
|
195
|
+
expectedMode: "introspection",
|
|
196
|
+
requiredPermission: "docs.write",
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Decision mode
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const result = await client.authorize(rawToken, "docs.write");
|
|
205
|
+
if (result.allowed) {
|
|
206
|
+
// proceed
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Decision mode is fail-closed: network errors return `{ allowed: false }`.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Token introspection (RFC 7662)
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
const result = await client.introspect(rawToken);
|
|
218
|
+
// result.active — boolean
|
|
219
|
+
// result.sub — string (when active)
|
|
220
|
+
// result.exp, iat, iss — standard claims
|
|
221
|
+
// result.scope — space-delimited string
|
|
222
|
+
// result.extra — all non-standard claims (includes roles, permissions, groups)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Introspection results are **never cached** — per RFC 7662, token state can change at any time.
|
|
226
|
+
|
|
227
|
+
Introspection mode is fail-closed: network errors or non-2xx responses from the introspection
|
|
228
|
+
endpoint cause the middleware to respond `401 Unauthorized`. The request is never forwarded to
|
|
229
|
+
the route handler on an indeterminate result.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Admin API
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { AdminClient } from "@hearth-auth/node";
|
|
237
|
+
|
|
238
|
+
const admin = new AdminClient({
|
|
239
|
+
base_url: "https://hearth.example.com",
|
|
240
|
+
realm_id: "<realm-id>",
|
|
241
|
+
access_token: adminToken, // must carry hearth.admin permission
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Users
|
|
245
|
+
const user = await admin.createUser({ email: "alice@example.com", display_name: "Alice" });
|
|
246
|
+
const page = await admin.listUsers({ limit: 50 });
|
|
247
|
+
// page.items: User[], page.next_cursor: string | null
|
|
248
|
+
await admin.deleteUser(user.id);
|
|
249
|
+
|
|
250
|
+
// Realms
|
|
251
|
+
const realm = await admin.createRealm({ name: "acme-corp" });
|
|
252
|
+
await admin.deleteRealm(realm.id);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Error types
|
|
258
|
+
|
|
259
|
+
All SDK errors extend `HearthError`. Import typed errors for precise handling:
|
|
260
|
+
|
|
261
|
+
| Error | When thrown |
|
|
262
|
+
|-------|-------------|
|
|
263
|
+
| `ConfigurationError` | Missing required config (e.g. `client_secret` needed for introspection) |
|
|
264
|
+
| `DiscoveryError` | OIDC discovery endpoint unreachable or returned invalid JSON |
|
|
265
|
+
| `JWKSFetchError` | JWKS endpoint unreachable or returned invalid response |
|
|
266
|
+
| `TokenExpiredError` | `exp` claim is in the past |
|
|
267
|
+
| `TokenNotYetValidError` | `nbf` claim is in the future |
|
|
268
|
+
| `TokenInvalidError` | Signature invalid, malformed JWT |
|
|
269
|
+
| `TokenIssuerError` | `iss` mismatch |
|
|
270
|
+
| `TokenAudienceError` | `aud` does not contain expected audience |
|
|
271
|
+
| `IntrospectionError` | Introspection endpoint unreachable or returned error |
|
|
272
|
+
| `RequiredActionError` | Token `token_type` is `"required_action"` |
|
|
273
|
+
| `AuthorizationModeError` | Server echoed a mode that differs from `expectedMode` |
|
|
274
|
+
| `AdminHttpError` | Admin API returned non-2xx |
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { HearthClient, TokenExpiredError, RequiredActionError } from "@hearth-auth/node";
|
|
278
|
+
|
|
279
|
+
const client = new HearthClient({
|
|
280
|
+
issuer_url: "https://hearth.example.com",
|
|
281
|
+
client_id: process.env.HEARTH_CLIENT_ID,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const token = await client.verifyToken(rawToken);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err instanceof RequiredActionError) {
|
|
288
|
+
// Token is valid but requires user to complete actions before using the API
|
|
289
|
+
console.log("Pending actions:", err.requiredActions); // string[]
|
|
290
|
+
// Redirect to err.redirectUri if present
|
|
291
|
+
} else if (err instanceof TokenExpiredError) {
|
|
292
|
+
// Ask client to refresh
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Troubleshooting
|
|
300
|
+
|
|
301
|
+
**`DiscoveryError`** — verify `issuer_url` is reachable and returns a valid `/.well-known/openid-configuration`.
|
|
302
|
+
|
|
303
|
+
**`JWKSFetchError`** — check network connectivity to the JWKS endpoint. The SDK re-fetches
|
|
304
|
+
once on a cache miss before returning this error.
|
|
305
|
+
|
|
306
|
+
**`TokenExpiredError`** — the token's `exp` claim is in the past. Ask the client to refresh.
|
|
307
|
+
|
|
308
|
+
**`TokenInvalidError`** — JWT signature does not match any key in the JWKS. If the server
|
|
309
|
+
recently rotated keys, call `client.invalidateCache()` and retry once.
|
|
310
|
+
|
|
311
|
+
**`TokenAudienceError`** — the token's `aud` claim does not contain the configured audience.
|
|
312
|
+
Verify `client_id` matches what your authorization server issues.
|
|
313
|
+
|
|
314
|
+
**`AuthorizationModeError`** — the server echoed a mode different from `expectedMode`. Verify
|
|
315
|
+
the `access_token_authorization` setting on the registered OAuth client matches your SDK config.
|
|
316
|
+
|
|
317
|
+
**`ConfigurationError: client_secret required`** — Introspection and Decision modes require
|
|
318
|
+
`client_secret`. Pass it in the `HearthClient` constructor.
|
|
319
|
+
|
|
320
|
+
See [docs/specs/SDK.md](../../docs/specs/SDK.md) Section 5 for the full error taxonomy.
|
package/dist/client.d.ts
CHANGED
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
import type { HearthConfig } from "./config.js";
|
|
3
3
|
import type { IntrospectionResult } from "./introspect.js";
|
|
4
4
|
import type { AuthorizeOptions, AuthorizeResult } from "./authorize.js";
|
|
5
|
+
import type { TokenResponse, DeviceAuthorizationResponse, UserInfoResponse, MePermissionsResponse, SvSnapshotResponse, SvDeltaResponse, ExchangeCodeOptions } from "./flows.js";
|
|
5
6
|
import type { VerifiedToken } from "./token.js";
|
|
6
7
|
export declare class HearthClient {
|
|
7
8
|
private readonly verifier;
|
|
8
9
|
private readonly introspectionClient;
|
|
9
10
|
private readonly authorizeClient;
|
|
10
11
|
private readonly discovery;
|
|
12
|
+
private readonly flows;
|
|
11
13
|
constructor(config: HearthConfig);
|
|
12
14
|
/**
|
|
13
|
-
* Verify a JWT using JWKS.
|
|
14
|
-
*
|
|
15
|
+
* Verify a JWT using JWKS-backed EdDSA/Ed25519 local signature verification (spec §2).
|
|
16
|
+
*
|
|
17
|
+
* Performs all five mandatory validation steps: signature, exp, iss, aud, iat.
|
|
15
18
|
* On key miss, re-fetches the JWKS once before failing (handles key rotation).
|
|
19
|
+
* Returns typed `VerifiedToken` on success; throws a typed §5 error on failure.
|
|
16
20
|
*/
|
|
17
21
|
verifyToken(token: string): Promise<VerifiedToken>;
|
|
18
22
|
/**
|
|
@@ -32,5 +36,85 @@ export declare class HearthClient {
|
|
|
32
36
|
* Call this after receiving a 401 from a resource server protected by the same issuer.
|
|
33
37
|
*/
|
|
34
38
|
invalidateCache(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Exchange an authorization code for tokens (RFC 6749 §4.1.3).
|
|
41
|
+
*
|
|
42
|
+
* Discovers the token endpoint from OIDC discovery. Sends credentials as
|
|
43
|
+
* `application/x-www-form-urlencoded` — never as query parameters.
|
|
44
|
+
*
|
|
45
|
+
* @param code - Authorization code from the callback URL.
|
|
46
|
+
* @param redirectUri - Same redirect_uri used in the authorization request.
|
|
47
|
+
* @param opts - Optional PKCE verifier (`codeVerifier`) for PKCE-protected flows.
|
|
48
|
+
*/
|
|
49
|
+
exchangeCode(code: string, redirectUri: string, opts?: ExchangeCodeOptions): Promise<TokenResponse>;
|
|
50
|
+
/**
|
|
51
|
+
* Obtain a token using the Client Credentials grant (RFC 6749 §4.4).
|
|
52
|
+
* Required for M2M authentication (services, daemons, admin tooling).
|
|
53
|
+
*
|
|
54
|
+
* @param scope - Optional space-delimited scope string.
|
|
55
|
+
*/
|
|
56
|
+
clientCredentials(scope?: string): Promise<TokenResponse>;
|
|
57
|
+
/**
|
|
58
|
+
* Begin a Device Authorization Flow (RFC 8628).
|
|
59
|
+
*
|
|
60
|
+
* Returns the `device_code`, `user_code`, `verification_uri`, and polling `interval`.
|
|
61
|
+
* Pass `device_code` and `interval` to `pollDeviceToken()` to await user approval.
|
|
62
|
+
*
|
|
63
|
+
* @param scope - Optional space-delimited scope string.
|
|
64
|
+
*/
|
|
65
|
+
startDeviceFlow(scope?: string): Promise<DeviceAuthorizationResponse>;
|
|
66
|
+
/**
|
|
67
|
+
* Poll the token endpoint until the device flow completes (RFC 8628 §3.5).
|
|
68
|
+
*
|
|
69
|
+
* Resolves with `TokenResponse` on user approval.
|
|
70
|
+
* Throws `TokenExpiredError` when the device code expires.
|
|
71
|
+
* Handles `authorization_pending` and `slow_down` internally — they are not surfaced.
|
|
72
|
+
*
|
|
73
|
+
* @param deviceCode - The `device_code` from `startDeviceFlow()`.
|
|
74
|
+
* @param intervalSeconds - Initial polling interval from `startDeviceFlow().interval`.
|
|
75
|
+
*/
|
|
76
|
+
pollDeviceToken(deviceCode: string, intervalSeconds: number): Promise<TokenResponse>;
|
|
77
|
+
/**
|
|
78
|
+
* Send a magic-link email for passwordless sign-in (spec §4.5.3).
|
|
79
|
+
*
|
|
80
|
+
* Always succeeds on 202 — enumeration resistant (server returns 202 whether or not
|
|
81
|
+
* the email is registered). HTTP 429 is surfaced as `OAuthFlowError`.
|
|
82
|
+
*
|
|
83
|
+
* Requires `realm_id` in `HearthConfig`.
|
|
84
|
+
*
|
|
85
|
+
* @param email - Email address to send the magic link to.
|
|
86
|
+
*/
|
|
87
|
+
requestMagicLink(email: string): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Fetch the OIDC userinfo claims for the bearer token (discovered endpoint).
|
|
90
|
+
*
|
|
91
|
+
* @param token - Access token whose claims to retrieve.
|
|
92
|
+
*/
|
|
93
|
+
userinfo(token: string): Promise<UserInfoResponse>;
|
|
94
|
+
/**
|
|
95
|
+
* Fetch the current user's live RBAC state from `GET /v1/me/permissions`.
|
|
96
|
+
*
|
|
97
|
+
* Unlike JWT embedded claims (cached at issuance), this reflects current server-side
|
|
98
|
+
* role and group assignments.
|
|
99
|
+
*
|
|
100
|
+
* @param token - Access token of the user whose permissions to retrieve.
|
|
101
|
+
*/
|
|
102
|
+
mePermissions(token: string): Promise<MePermissionsResponse>;
|
|
103
|
+
/**
|
|
104
|
+
* Fetch the full session-version snapshot (HEA-930).
|
|
105
|
+
* Returns all current `{sessionId → minSV}` pairs. Seed a local cache on startup.
|
|
106
|
+
* Requires a token with the `hearth.sv_feed` scope.
|
|
107
|
+
*/
|
|
108
|
+
svSnapshot(token: string): Promise<SvSnapshotResponse>;
|
|
109
|
+
/**
|
|
110
|
+
* Fetch session-version deltas since sequence number `since` (HEA-930).
|
|
111
|
+
* Returns `null` when there are no new deltas (HTTP 204 No Content).
|
|
112
|
+
* Requires a token with the `hearth.sv_feed` scope.
|
|
113
|
+
*
|
|
114
|
+
* @param token - Service token with `hearth.sv_feed` scope.
|
|
115
|
+
* @param since - Return only events with `seq > since`.
|
|
116
|
+
* @param limit - Maximum number of deltas to return.
|
|
117
|
+
*/
|
|
118
|
+
svDelta(token: string, since: number, limit?: number): Promise<SvDeltaResponse | null>;
|
|
35
119
|
}
|
|
36
120
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAE5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAE5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,KAAK,EACV,aAAa,EACb,2BAA2B,EAC3B,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,eAAe,EACf,mBAAmB,EACpB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAsB;IAC1D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;gBAE7B,MAAM,EAAE,YAAY;IAShC;;;;;;OAMG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAIxD;;;OAGG;IACG,UAAU,CACd,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,cAAc,GAAG,eAAe,GAC/C,OAAO,CAAC,mBAAmB,CAAC;IAI/B;;;;;OAKG;IACG,SAAS,CACb,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,eAAe,CAAC;IAI3B;;;OAGG;IACH,eAAe,IAAI,IAAI;IAMvB;;;;;;;;;OASG;IACG,YAAY,CAChB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,aAAa,CAAC;IAIzB;;;;;OAKG;IACG,iBAAiB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAI/D;;;;;;;OAOG;IACG,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAI3E;;;;;;;;;OASG;IACG,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAI1F;;;;;;;;;OASG;IACG,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpD;;;;OAIG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIxD;;;;;;;OAOG;IACG,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAMlE;;;;OAIG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAI5D;;;;;;;;OAQG;IACG,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;CAG7F"}
|
package/dist/client.js
CHANGED
|
@@ -4,22 +4,27 @@ import { DiscoveryClient } from "./discovery.js";
|
|
|
4
4
|
import { JwksVerifier } from "./jwks.js";
|
|
5
5
|
import { IntrospectionClient } from "./introspect.js";
|
|
6
6
|
import { AuthorizeClient } from "./authorize.js";
|
|
7
|
+
import { OAuthFlowsClient } from "./flows.js";
|
|
7
8
|
export class HearthClient {
|
|
8
9
|
verifier;
|
|
9
10
|
introspectionClient;
|
|
10
11
|
authorizeClient;
|
|
11
12
|
discovery;
|
|
13
|
+
flows;
|
|
12
14
|
constructor(config) {
|
|
13
15
|
const resolved = resolveConfig(config);
|
|
14
16
|
this.discovery = new DiscoveryClient(resolved.issuer_url, resolved.http_timeout);
|
|
15
17
|
this.verifier = new JwksVerifier(resolved, this.discovery);
|
|
16
18
|
this.introspectionClient = new IntrospectionClient(resolved, () => this.discovery.discover());
|
|
17
19
|
this.authorizeClient = new AuthorizeClient(resolved);
|
|
20
|
+
this.flows = new OAuthFlowsClient(resolved, () => this.discovery.discover());
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
20
|
-
* Verify a JWT using JWKS.
|
|
21
|
-
*
|
|
23
|
+
* Verify a JWT using JWKS-backed EdDSA/Ed25519 local signature verification (spec §2).
|
|
24
|
+
*
|
|
25
|
+
* Performs all five mandatory validation steps: signature, exp, iss, aud, iat.
|
|
22
26
|
* On key miss, re-fetches the JWKS once before failing (handles key rotation).
|
|
27
|
+
* Returns typed `VerifiedToken` on success; throws a typed §5 error on failure.
|
|
23
28
|
*/
|
|
24
29
|
async verifyToken(token) {
|
|
25
30
|
return this.verifier.verifyToken(token);
|
|
@@ -47,5 +52,106 @@ export class HearthClient {
|
|
|
47
52
|
invalidateCache() {
|
|
48
53
|
this.verifier.invalidateCache();
|
|
49
54
|
}
|
|
55
|
+
// ── §4.5 OAuth Flows ───────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Exchange an authorization code for tokens (RFC 6749 §4.1.3).
|
|
58
|
+
*
|
|
59
|
+
* Discovers the token endpoint from OIDC discovery. Sends credentials as
|
|
60
|
+
* `application/x-www-form-urlencoded` — never as query parameters.
|
|
61
|
+
*
|
|
62
|
+
* @param code - Authorization code from the callback URL.
|
|
63
|
+
* @param redirectUri - Same redirect_uri used in the authorization request.
|
|
64
|
+
* @param opts - Optional PKCE verifier (`codeVerifier`) for PKCE-protected flows.
|
|
65
|
+
*/
|
|
66
|
+
async exchangeCode(code, redirectUri, opts) {
|
|
67
|
+
return this.flows.exchangeCode(code, redirectUri, opts);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Obtain a token using the Client Credentials grant (RFC 6749 §4.4).
|
|
71
|
+
* Required for M2M authentication (services, daemons, admin tooling).
|
|
72
|
+
*
|
|
73
|
+
* @param scope - Optional space-delimited scope string.
|
|
74
|
+
*/
|
|
75
|
+
async clientCredentials(scope) {
|
|
76
|
+
return this.flows.clientCredentials(scope);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Begin a Device Authorization Flow (RFC 8628).
|
|
80
|
+
*
|
|
81
|
+
* Returns the `device_code`, `user_code`, `verification_uri`, and polling `interval`.
|
|
82
|
+
* Pass `device_code` and `interval` to `pollDeviceToken()` to await user approval.
|
|
83
|
+
*
|
|
84
|
+
* @param scope - Optional space-delimited scope string.
|
|
85
|
+
*/
|
|
86
|
+
async startDeviceFlow(scope) {
|
|
87
|
+
return this.flows.startDeviceFlow(scope);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Poll the token endpoint until the device flow completes (RFC 8628 §3.5).
|
|
91
|
+
*
|
|
92
|
+
* Resolves with `TokenResponse` on user approval.
|
|
93
|
+
* Throws `TokenExpiredError` when the device code expires.
|
|
94
|
+
* Handles `authorization_pending` and `slow_down` internally — they are not surfaced.
|
|
95
|
+
*
|
|
96
|
+
* @param deviceCode - The `device_code` from `startDeviceFlow()`.
|
|
97
|
+
* @param intervalSeconds - Initial polling interval from `startDeviceFlow().interval`.
|
|
98
|
+
*/
|
|
99
|
+
async pollDeviceToken(deviceCode, intervalSeconds) {
|
|
100
|
+
return this.flows.pollDeviceToken(deviceCode, intervalSeconds);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Send a magic-link email for passwordless sign-in (spec §4.5.3).
|
|
104
|
+
*
|
|
105
|
+
* Always succeeds on 202 — enumeration resistant (server returns 202 whether or not
|
|
106
|
+
* the email is registered). HTTP 429 is surfaced as `OAuthFlowError`.
|
|
107
|
+
*
|
|
108
|
+
* Requires `realm_id` in `HearthConfig`.
|
|
109
|
+
*
|
|
110
|
+
* @param email - Email address to send the magic link to.
|
|
111
|
+
*/
|
|
112
|
+
async requestMagicLink(email) {
|
|
113
|
+
return this.flows.requestMagicLink(email);
|
|
114
|
+
}
|
|
115
|
+
// ── §4 UserInfo & Permissions ──────────────────────────────────────────────
|
|
116
|
+
/**
|
|
117
|
+
* Fetch the OIDC userinfo claims for the bearer token (discovered endpoint).
|
|
118
|
+
*
|
|
119
|
+
* @param token - Access token whose claims to retrieve.
|
|
120
|
+
*/
|
|
121
|
+
async userinfo(token) {
|
|
122
|
+
return this.flows.userinfo(token);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Fetch the current user's live RBAC state from `GET /v1/me/permissions`.
|
|
126
|
+
*
|
|
127
|
+
* Unlike JWT embedded claims (cached at issuance), this reflects current server-side
|
|
128
|
+
* role and group assignments.
|
|
129
|
+
*
|
|
130
|
+
* @param token - Access token of the user whose permissions to retrieve.
|
|
131
|
+
*/
|
|
132
|
+
async mePermissions(token) {
|
|
133
|
+
return this.flows.mePermissions(token);
|
|
134
|
+
}
|
|
135
|
+
// ── §HEA-930 Session-version feed ─────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Fetch the full session-version snapshot (HEA-930).
|
|
138
|
+
* Returns all current `{sessionId → minSV}` pairs. Seed a local cache on startup.
|
|
139
|
+
* Requires a token with the `hearth.sv_feed` scope.
|
|
140
|
+
*/
|
|
141
|
+
async svSnapshot(token) {
|
|
142
|
+
return this.flows.svSnapshot(token);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Fetch session-version deltas since sequence number `since` (HEA-930).
|
|
146
|
+
* Returns `null` when there are no new deltas (HTTP 204 No Content).
|
|
147
|
+
* Requires a token with the `hearth.sv_feed` scope.
|
|
148
|
+
*
|
|
149
|
+
* @param token - Service token with `hearth.sv_feed` scope.
|
|
150
|
+
* @param since - Return only events with `seq > since`.
|
|
151
|
+
* @param limit - Maximum number of deltas to return.
|
|
152
|
+
*/
|
|
153
|
+
async svDelta(token, since, limit) {
|
|
154
|
+
return this.flows.svDelta(token, since, limit);
|
|
155
|
+
}
|
|
50
156
|
}
|
|
51
157
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAG5E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.test.js
CHANGED
|
@@ -3,6 +3,7 @@ import { HearthClient } from "./client.js";
|
|
|
3
3
|
import { JwksVerifier } from "./jwks.js";
|
|
4
4
|
import { IntrospectionClient } from "./introspect.js";
|
|
5
5
|
import { AuthorizeClient } from "./authorize.js";
|
|
6
|
+
import { OAuthFlowsClient } from "./flows.js";
|
|
6
7
|
import { VerifiedToken } from "./token.js";
|
|
7
8
|
const CONFIG = {
|
|
8
9
|
issuer_url: "https://auth.example.com",
|
|
@@ -57,4 +58,86 @@ describe("HearthClient", () => {
|
|
|
57
58
|
expect(spy).toHaveBeenCalledOnce();
|
|
58
59
|
});
|
|
59
60
|
});
|
|
61
|
+
// ── OAuth flow delegation ─────────────────────────────────────────────────────
|
|
62
|
+
const TOKEN_RESPONSE = {
|
|
63
|
+
access_token: "eyJ.at",
|
|
64
|
+
token_type: "Bearer",
|
|
65
|
+
expires_in: 3600,
|
|
66
|
+
};
|
|
67
|
+
const DEVICE_RESPONSE = {
|
|
68
|
+
device_code: "dev-code",
|
|
69
|
+
user_code: "WXYZ-1234",
|
|
70
|
+
verification_uri: "https://auth.example.com/activate",
|
|
71
|
+
expires_in: 600,
|
|
72
|
+
interval: 5,
|
|
73
|
+
};
|
|
74
|
+
describe("HearthClient — OAuth flow delegation", () => {
|
|
75
|
+
afterEach(() => vi.restoreAllMocks());
|
|
76
|
+
it("delegates exchangeCode to OAuthFlowsClient", async () => {
|
|
77
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "exchangeCode").mockResolvedValue(TOKEN_RESPONSE);
|
|
78
|
+
const client = new HearthClient(CONFIG);
|
|
79
|
+
const result = await client.exchangeCode("code-abc", "https://app.local/cb", { codeVerifier: "v3r" });
|
|
80
|
+
expect(spy).toHaveBeenCalledWith("code-abc", "https://app.local/cb", { codeVerifier: "v3r" });
|
|
81
|
+
expect(result).toBe(TOKEN_RESPONSE);
|
|
82
|
+
});
|
|
83
|
+
it("delegates clientCredentials to OAuthFlowsClient", async () => {
|
|
84
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "clientCredentials").mockResolvedValue(TOKEN_RESPONSE);
|
|
85
|
+
const client = new HearthClient(CONFIG);
|
|
86
|
+
const result = await client.clientCredentials("openid profile");
|
|
87
|
+
expect(spy).toHaveBeenCalledWith("openid profile");
|
|
88
|
+
expect(result).toBe(TOKEN_RESPONSE);
|
|
89
|
+
});
|
|
90
|
+
it("delegates startDeviceFlow to OAuthFlowsClient", async () => {
|
|
91
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "startDeviceFlow").mockResolvedValue(DEVICE_RESPONSE);
|
|
92
|
+
const client = new HearthClient(CONFIG);
|
|
93
|
+
const result = await client.startDeviceFlow("openid");
|
|
94
|
+
expect(spy).toHaveBeenCalledWith("openid");
|
|
95
|
+
expect(result).toBe(DEVICE_RESPONSE);
|
|
96
|
+
});
|
|
97
|
+
it("delegates pollDeviceToken to OAuthFlowsClient", async () => {
|
|
98
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "pollDeviceToken").mockResolvedValue(TOKEN_RESPONSE);
|
|
99
|
+
const client = new HearthClient(CONFIG);
|
|
100
|
+
const result = await client.pollDeviceToken("dev-code", 5);
|
|
101
|
+
expect(spy).toHaveBeenCalledWith("dev-code", 5);
|
|
102
|
+
expect(result).toBe(TOKEN_RESPONSE);
|
|
103
|
+
});
|
|
104
|
+
it("delegates requestMagicLink to OAuthFlowsClient", async () => {
|
|
105
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "requestMagicLink").mockResolvedValue(undefined);
|
|
106
|
+
const client = new HearthClient({ ...CONFIG, realm_id: "realm1" });
|
|
107
|
+
await client.requestMagicLink("user@example.com");
|
|
108
|
+
expect(spy).toHaveBeenCalledWith("user@example.com");
|
|
109
|
+
});
|
|
110
|
+
it("delegates userinfo to OAuthFlowsClient", async () => {
|
|
111
|
+
const uiResp = { sub: "user1", email: "user@example.com" };
|
|
112
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "userinfo").mockResolvedValue(uiResp);
|
|
113
|
+
const client = new HearthClient(CONFIG);
|
|
114
|
+
const result = await client.userinfo("access-tok");
|
|
115
|
+
expect(spy).toHaveBeenCalledWith("access-tok");
|
|
116
|
+
expect(result).toBe(uiResp);
|
|
117
|
+
});
|
|
118
|
+
it("delegates mePermissions to OAuthFlowsClient", async () => {
|
|
119
|
+
const permResp = { roles: ["admin"], groups: [], permissions: ["docs.write"] };
|
|
120
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "mePermissions").mockResolvedValue(permResp);
|
|
121
|
+
const client = new HearthClient(CONFIG);
|
|
122
|
+
const result = await client.mePermissions("access-tok");
|
|
123
|
+
expect(spy).toHaveBeenCalledWith("access-tok");
|
|
124
|
+
expect(result).toBe(permResp);
|
|
125
|
+
});
|
|
126
|
+
it("delegates svSnapshot to OAuthFlowsClient", async () => {
|
|
127
|
+
const snap = { realm: "r", current_seq: 10, versions: {} };
|
|
128
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "svSnapshot").mockResolvedValue(snap);
|
|
129
|
+
const client = new HearthClient(CONFIG);
|
|
130
|
+
const result = await client.svSnapshot("svc-tok");
|
|
131
|
+
expect(spy).toHaveBeenCalledWith("svc-tok");
|
|
132
|
+
expect(result).toBe(snap);
|
|
133
|
+
});
|
|
134
|
+
it("delegates svDelta to OAuthFlowsClient", async () => {
|
|
135
|
+
const delta = { realm: "r", next_seq: 5, deltas: [] };
|
|
136
|
+
const spy = vi.spyOn(OAuthFlowsClient.prototype, "svDelta").mockResolvedValue(delta);
|
|
137
|
+
const client = new HearthClient(CONFIG);
|
|
138
|
+
const result = await client.svDelta("svc-tok", 3, 50);
|
|
139
|
+
expect(spy).toHaveBeenCalledWith("svc-tok", 3, 50);
|
|
140
|
+
expect(result).toBe(delta);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
60
143
|
//# sourceMappingURL=client.test.js.map
|