@hawcx/oauth-client 0.2.0 โ†’ 5.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.
Files changed (74) hide show
  1. package/README.md +587 -204
  2. package/dist/client-assertion.d.ts +45 -0
  3. package/dist/client-assertion.d.ts.map +1 -0
  4. package/dist/client-assertion.js +141 -0
  5. package/dist/client-assertion.js.map +1 -0
  6. package/dist/delegation/credentials.d.ts +92 -0
  7. package/dist/delegation/credentials.d.ts.map +1 -0
  8. package/dist/delegation/credentials.js +215 -0
  9. package/dist/delegation/credentials.js.map +1 -0
  10. package/dist/delegation/crypto.d.ts +2 -2
  11. package/dist/delegation/crypto.d.ts.map +1 -1
  12. package/dist/delegation/crypto.js +41 -32
  13. package/dist/delegation/crypto.js.map +1 -1
  14. package/dist/delegation/exceptions.d.ts +12 -5
  15. package/dist/delegation/exceptions.d.ts.map +1 -1
  16. package/dist/delegation/exceptions.js +12 -14
  17. package/dist/delegation/exceptions.js.map +1 -1
  18. package/dist/delegation/factory.d.ts +20 -0
  19. package/dist/delegation/factory.d.ts.map +1 -0
  20. package/dist/delegation/factory.js +49 -0
  21. package/dist/delegation/factory.js.map +1 -0
  22. package/dist/delegation/hawcx.d.ts +59 -224
  23. package/dist/delegation/hawcx.d.ts.map +1 -1
  24. package/dist/delegation/hawcx.js +99 -299
  25. package/dist/delegation/hawcx.js.map +1 -1
  26. package/dist/delegation/index.d.ts +4 -9
  27. package/dist/delegation/index.d.ts.map +1 -1
  28. package/dist/delegation/index.js +5 -17
  29. package/dist/delegation/index.js.map +1 -1
  30. package/dist/delegation/transport.d.ts +40 -0
  31. package/dist/delegation/transport.d.ts.map +1 -0
  32. package/dist/delegation/transport.js +120 -0
  33. package/dist/delegation/transport.js.map +1 -0
  34. package/dist/errors.d.ts +62 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +73 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/index.d.ts +12 -15
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +20 -27
  41. package/dist/index.js.map +1 -1
  42. package/dist/oauth.d.ts +265 -0
  43. package/dist/oauth.d.ts.map +1 -0
  44. package/dist/oauth.js +497 -0
  45. package/dist/oauth.js.map +1 -0
  46. package/dist/stepup/client.d.ts +26 -0
  47. package/dist/stepup/client.d.ts.map +1 -0
  48. package/dist/stepup/client.js +78 -0
  49. package/dist/stepup/client.js.map +1 -0
  50. package/dist/stepup/index.d.ts +3 -0
  51. package/dist/stepup/index.d.ts.map +1 -0
  52. package/dist/stepup/index.js +19 -0
  53. package/dist/stepup/index.js.map +1 -0
  54. package/dist/stepup/types.d.ts +35 -0
  55. package/dist/stepup/types.d.ts.map +1 -0
  56. package/dist/stepup/types.js +3 -0
  57. package/dist/stepup/types.js.map +1 -0
  58. package/package.json +13 -16
  59. package/dist/client.d.ts +0 -134
  60. package/dist/client.d.ts.map +0 -1
  61. package/dist/client.js +0 -300
  62. package/dist/client.js.map +0 -1
  63. package/dist/delegation/client.d.ts +0 -116
  64. package/dist/delegation/client.d.ts.map +0 -1
  65. package/dist/delegation/client.js +0 -227
  66. package/dist/delegation/client.js.map +0 -1
  67. package/dist/exceptions.d.ts +0 -52
  68. package/dist/exceptions.d.ts.map +0 -1
  69. package/dist/exceptions.js +0 -81
  70. package/dist/exceptions.js.map +0 -1
  71. package/dist/utils.d.ts +0 -61
  72. package/dist/utils.d.ts.map +0 -1
  73. package/dist/utils.js +0 -263
  74. package/dist/utils.js.map +0 -1
package/README.md CHANGED
@@ -1,310 +1,693 @@
1
- # Hawcx OAuth Client SDK (Node.js)
1
+ # @hawcx/oauth-client
2
2
 
3
- A lightweight, production-ready Node.js/TypeScript library for exchanging OAuth authorization codes and verifying JWT claims. Built with security, reliability, and ease of use in mind.
3
+ Simple, production-ready OAuth client SDK for Hawcx authentication.
4
4
 
5
5
  ## Features
6
6
 
7
- - ๐Ÿ” **Secure JWT Verification**: RS256/ES256/EdDSA signature verification with configurable validation
8
- - ๐Ÿ†• **PKCE Support**: Native PKCE (RFC 7636) support for enhanced OAuth security
9
- - ๐Ÿš€ **Simple API**: One function call to go from code to verified claims
10
- - ๐ŸŽฏ **High-Level Hawcx Client**: One-line MFA setup with automatic encryption/signing
11
- - ๐Ÿ›ก๏ธ **Comprehensive Error Handling**: Clear, actionable error messages for all failure scenarios
12
- - ๐Ÿ“ **TypeScript First**: Full type definitions for better IDE support and type checking
13
- - ๐Ÿงช **Production Ready**: Extensive error handling and security best practices
14
- - ๐Ÿ”ง **Express/Fastify Compatible**: Works with any Node.js framework
15
- - ๐Ÿ” **Detailed Logging**: Structured logging for debugging (no sensitive data logged)
7
+ - **OIDC Discovery** - `HawcxOAuth.fromIssuer(...)` resolves endpoints from `/.well-known/openid-configuration` and enforces signature + `iss` + `aud` + `exp` + `nbf` on verify
8
+ - **Simple JWT Verification** - Uses `jose` library for automatic JWKS caching and signature handling
9
+ - **PKCE Support** - Native PKCE (RFC 7636) support for enhanced OAuth security
10
+ - **Delegation API** - Server-to-server user management (MFA, devices)
11
+ - **Step-Up Auth** - Step-up authentication flows for sensitive operations
12
+ - **TypeScript First** - Full type definitions included
13
+ - **Zero Config Crypto** - All cryptographic complexity hidden behind a single secret key
16
14
 
17
15
  ## Installation
18
16
 
19
17
  ```bash
20
18
  npm install @hawcx/oauth-client
19
+ ```
20
+
21
+ ## Quick Start (OIDC discovery, recommended)
22
+
23
+ `HawcxOAuth.fromIssuer(...)` does the discovery fetch once at startup and
24
+ gives you a client whose `exchangeCode` + `verifyToken` enforce all the
25
+ standard OIDC claim checks. Construct once and reuse.
26
+
27
+ ```typescript
28
+ import { HawcxOAuth } from '@hawcx/oauth-client';
29
+
30
+ const oauth = await HawcxOAuth.fromIssuer({
31
+ issuer: 'https://api.hawcx.com', // โ†’ discovery + expected `iss`
32
+ configId: process.env.HAWCX_CONFIG_ID!, // โ†’ X-Config-Id header
33
+ clientId: process.env.HAWCX_CLIENT_ID!, // โ†’ expected `aud` on id_tokens
34
+ });
35
+
36
+ const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
37
+ console.log(claims.sub); // user ID
38
+ console.log(claims.email); // user email
39
+ ```
40
+
41
+ > **`configId` vs `clientId` โ€” they are different values.**
42
+ > Hawcx separates the tenant routing key from the audience claim:
43
+ >
44
+ > - **`configId`** โ€” the per-tenant config identifier. Sent as the
45
+ > `X-Config-Id` header on the token-exchange POST so the OAuth server
46
+ > can look up the tenant.
47
+ > - **`clientId`** โ€” the value the tenant has configured (in the Hawcx
48
+ > admin console) as the `aud` claim on issued id_tokens. The verifier
49
+ > matches the token's `aud` against this exact string.
50
+ >
51
+ > Passing the same value for both fails verification with
52
+ > `Unexpected JWT audience` unless your tenant happens to use identical
53
+ > strings.
54
+
55
+ ## Quick Start (legacy)
56
+
57
+ The legacy single-arg constructor still works for backward compatibility
58
+ with 4.x callers. It verifies the JWT signature only โ€” `iss` and `aud`
59
+ are not checked. Prefer `fromIssuer` for new code.
60
+
61
+ ```typescript
62
+ import { HawcxOAuth } from '@hawcx/oauth-client';
21
63
 
22
- # or with yarn
23
- yarn add @hawcx/oauth-client
64
+ const oauth = new HawcxOAuth({ configId: 'your-config-id' });
24
65
 
25
- # or with pnpm
26
- pnpm add @hawcx/oauth-client
66
+ const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
27
67
  ```
28
68
 
29
- ## Quick Start
69
+ ---
70
+
71
+ ## Confidential clients (`private_key_jwt`)
72
+
73
+ If your project is registered in the Hawcx admin console with **Client
74
+ authentication = Signing key (`private_key_jwt`)**, attach an Ed25519 signer.
75
+ The SDK then signs an RFC 7523 `client_assertion` on every token exchange
76
+ (PKCE is still sent). Public clients omit this and keep using PKCE only.
77
+
78
+ ```typescript
79
+ import { HawcxOAuth, ClientAssertionSigner } from '@hawcx/oauth-client';
80
+
81
+ const privateJwk = JSON.parse(process.env.HAWCX_PRIVATE_JWK!); // OKP/Ed25519, with kid
82
+
83
+ const oauth = (
84
+ await HawcxOAuth.fromIssuer({
85
+ issuer: 'https://dev-demo-api.hawcx.com',
86
+ configId: process.env.HAWCX_CONFIG_ID!,
87
+ clientId: process.env.HAWCX_CLIENT_ID!,
88
+ })
89
+ ).withClientAssertion(ClientAssertionSigner.ed25519FromJwk(privateJwk));
90
+
91
+ const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
92
+ ```
93
+
94
+ The private key must be Ed25519 (`kty: 'OKP', crv: 'Ed25519'`, with a `kid`);
95
+ its public JWK is what you register in the admin console. The assertion's `aud`
96
+ is the discovered `token_endpoint`, and EdDSA is the only accepted algorithm.
97
+ `clientId` must be the confidential client's id โ€” it becomes the assertion
98
+ `iss`/`sub` and the id_token `aud`.
99
+
100
+ > If you bound a `nonce` at `/authorize`, pass it as the 4th argument to
101
+ > `exchangeCode(code, verifier, redirectUri?, expectedNonce)` โ€” verification
102
+ > refuses a nonce-bearing token when none is supplied (OIDC Core ยง3.1.3.7).
103
+
104
+ ---
105
+
106
+ ## Modules
30
107
 
31
- ### OAuth Code Exchange
108
+ | Module | Purpose |
109
+ |--------|---------|
110
+ | **OAuth** | Token exchange and JWT verification |
111
+ | **Delegation** | Server-to-server user management (MFA, devices) |
112
+ | **Step-Up** | Step-up authentication flows |
113
+
114
+ ---
115
+
116
+ ## OAuth Module
117
+
118
+ Core module for OAuth 2.1 + PKCE authentication.
119
+
120
+ ### Construction
121
+
122
+ #### Discovery mode (recommended)
32
123
 
33
124
  ```typescript
34
- import { exchangeCodeForClaims } from '@hawcx/oauth-client';
125
+ import { HawcxOAuth } from '@hawcx/oauth-client';
126
+
127
+ const oauth = await HawcxOAuth.fromIssuer({
128
+ issuer: 'https://api.hawcx.com', // Required: discovery + expected `iss`
129
+ configId: 'your-config-id', // Required: X-Config-Id header
130
+ clientId: 'your-client-id', // Required: expected `aud` on id_tokens
131
+ timeout: 10000, // Optional: per-request timeout in ms
132
+ discoveryTimeout: 5000, // Optional: discovery fetch timeout in ms
133
+ });
35
134
 
36
- // Exchange authorization code for verified claims
37
- const claims = await exchangeCodeForClaims({
38
- code: req.body.code,
39
- oauthTokenUrl: process.env.OAUTH_TOKEN_ENDPOINT!,
40
- clientId: process.env.OAUTH_CLIENT_ID!,
41
- apiKey: process.env.HAWCX_API_KEY!,
42
- publicKey: process.env.OAUTH_PUBLIC_KEY!,
43
- // Optional (recommended for production):
44
- codeVerifier: session.pkceVerifier, // PKCE support
45
- audience: 'my-app', // Validate 'aud' claim
46
- issuer: 'https://oauth.example.com', // Validate 'iss' claim
47
- leeway: 10 // Clock skew tolerance
135
+ oauth.discoveryMetadata; // resolved OIDC discovery doc, for diagnostics
136
+ ```
137
+
138
+ Throws `DiscoveryError` if the discovery document is unreachable, returns
139
+ non-2xx, isn't valid JSON, or is missing any of `token_endpoint`,
140
+ `jwks_uri`, `id_token_signing_alg_values_supported`.
141
+
142
+ #### Legacy mode
143
+
144
+ ```typescript
145
+ const oauth = new HawcxOAuth({
146
+ configId: 'your-config-id', // Required
147
+ baseUrl: 'https://api.hawcx.com', // Optional: defaults to api.hawcx.com
148
+ timeout: 10000, // Optional
48
149
  });
150
+ ```
151
+
152
+ Hits the fixed endpoints `<baseUrl>/oauth2/token` and `<baseUrl>/keys`
153
+ directly. **Signature-only verification** โ€” `iss` and `aud` are not
154
+ checked. Kept for backward compatibility with 4.x callers.
155
+
156
+ ### Methods
157
+
158
+ #### `exchangeCode(code, codeVerifier, redirectUri?): Promise<ExchangeResult>`
159
+
160
+ Exchange an authorization code for tokens.
161
+
162
+ ```typescript
163
+ const { idToken, claims } = await oauth.exchangeCode(
164
+ 'authorization-code',
165
+ 'pkce-code-verifier'
166
+ );
167
+
168
+ // With redirect_uri (RFC 6749 ยง4.1.3) when your auth request registered one:
169
+ const { idToken, claims } = await oauth.exchangeCode(
170
+ 'authorization-code',
171
+ 'pkce-code-verifier',
172
+ 'https://app.example.com/auth/callback'
173
+ );
174
+ ```
175
+
176
+ **Returns:**
177
+ ```typescript
178
+ interface ExchangeResult {
179
+ idToken: string; // Raw JWT token
180
+ claims: JwtClaims; // Verified claims
181
+ }
182
+ ```
183
+
184
+ #### `verifyToken(token, options?): Promise<JwtClaims>`
49
185
 
50
- // Use the verified claims
51
- const userId = claims.sub;
52
- // Mint your own access token (SDK only verifies, doesn't mint)
186
+ Verify an existing JWT token. In discovery mode, enforces signature + `iss` +
187
+ `aud` + `exp` + `nbf`. In legacy mode, only the signature is checked.
188
+
189
+ ```typescript
190
+ const claims = await oauth.verifyToken(idToken);
191
+
192
+ // With expected nonce (useful for flows that bind a nonce to the auth request):
193
+ const claims = await oauth.verifyToken(idToken, { nonce: 'expected-nonce' });
194
+ ```
195
+
196
+ **Returns:**
197
+ ```typescript
198
+ interface JwtClaims {
199
+ sub?: string; // User ID
200
+ iss?: string; // Issuer
201
+ aud?: string | string[]; // Audience
202
+ iat?: number; // Issued at (unix timestamp)
203
+ exp?: number; // Expiration (unix timestamp)
204
+ email?: string; // User email
205
+ name?: string; // User name
206
+ amr?: string[]; // Authentication methods
207
+ mfa_method?: string; // MFA method used
208
+ nonce?: string; // Nonce (if bound)
209
+ }
210
+ ```
211
+
212
+ #### `refreshJwks(): void`
213
+
214
+ Reset the JWKS cache so the next verification refetches keys (useful for testing
215
+ or key rotation).
216
+
217
+ ```typescript
218
+ oauth.refreshJwks();
219
+ ```
220
+
221
+ > **Deprecated:** `clearCache()` is a deprecated passthrough that calls
222
+ > `refreshJwks()` (and emits a one-time `console.warn`). It still works but will be
223
+ > removed in 6.0.0 โ€” prefer `refreshJwks()`.
224
+
225
+ ### Errors
226
+
227
+ ```typescript
228
+ import {
229
+ HawcxOAuthError, // Base error class
230
+ DiscoveryError, // OIDC discovery resolution failed (fromIssuer only)
231
+ TokenExchangeError, // Code exchange failed
232
+ TokenVerificationError // JWT verification failed
233
+ } from '@hawcx/oauth-client';
234
+
235
+ try {
236
+ const oauth = await HawcxOAuth.fromIssuer({ issuer, configId, clientId });
237
+ await oauth.exchangeCode(code, verifier);
238
+ } catch (error) {
239
+ if (error instanceof DiscoveryError) {
240
+ console.log(error.cause); // original network / parse error
241
+ }
242
+ if (error instanceof TokenExchangeError) {
243
+ console.log(error.statusCode); // HTTP status code
244
+ }
245
+ if (error instanceof TokenVerificationError) {
246
+ // "Token expired", "Invalid signature", "Unexpected JWT audience: ...", etc.
247
+ console.log(error.message);
248
+ }
249
+ }
53
250
  ```
54
251
 
55
- ### Hawcx Delegation (MFA Setup)
252
+ ---
253
+
254
+ ## Delegation Module
56
255
 
57
- For Hawcx MFA setup and user management, use the delegation client:
256
+ Server-to-server API for managing users, MFA, and devices. Requires a Hawcx secret key.
257
+
258
+ ### `DelegationClient`
58
259
 
59
260
  ```typescript
60
- import { HawcxDelegationClient, MfaMethod } from '@hawcx/oauth-client';
261
+ import { DelegationClient, MfaMethod } from '@hawcx/oauth-client';
262
+
263
+ const client = DelegationClient.fromSecretKey({
264
+ baseUrl: 'https://api.hawcx.com',
265
+ secretKey: process.env.HAWCX_SECRET_KEY!,
266
+ apiKey: 'optional-api-key', // Optional
267
+ timeoutSeconds: 15, // Optional (default: 15)
268
+ clockSkewSeconds: 300, // Optional (default: 300)
269
+ });
270
+ ```
61
271
 
62
- // ๐ŸŽ‰ One-line initialization from explicit keys!
63
- const client = HawcxDelegationClient.fromKeys({
64
- spSigningKey: process.env.SP_ED25519_PRIVATE_KEY_PEM!,
65
- spEncryptionKey: process.env.SP_X25519_PRIVATE_KEY_PEM!,
66
- idpVerifyKey: process.env.IDP_ED25519_PUBLIC_KEY_PEM!,
67
- idpEncryptionKey: process.env.IDP_X25519_PUBLIC_KEY_PEM!,
68
- baseUrl: 'https://example.hawcx.com',
69
- spId: process.env.OAUTH_CLIENT_ID!
272
+ ### MFA Operations
273
+
274
+ #### `client.mfa.initiate(options): Promise<object>`
275
+
276
+ Initiate MFA setup or change for a user.
277
+
278
+ ```typescript
279
+ // Email MFA
280
+ const result = await client.mfa.initiate({
281
+ userId: 'user@example.com',
282
+ mfaMethod: MfaMethod.EMAIL,
70
283
  });
71
284
 
72
- // Initiate MFA setup (Email, SMS, or TOTP)
73
- const result = await client.initiateMfaChange({
74
- userid: "user@example.com",
75
- mfaMethod: MfaMethod.SMS,
76
- phoneNumber: "+15551234567"
285
+ // SMS MFA (requires phone number)
286
+ const result = await client.mfa.initiate({
287
+ userId: 'user@example.com',
288
+ mfaMethod: MfaMethod.SMS,
289
+ phoneNumber: '+1234567890',
77
290
  });
78
291
 
79
- // Verify OTP and complete MFA setup
80
- await client.verifyMfaChange({
81
- userid: "user@example.com",
82
- sessionId: result.session_id,
83
- otp: "123456"
292
+ // TOTP MFA
293
+ const result = await client.mfa.initiate({
294
+ userId: 'user@example.com',
295
+ mfaMethod: MfaMethod.TOTP,
84
296
  });
85
297
 
86
- // Get user credentials
87
- const creds = await client.getUserCredentials("user@example.com");
88
- console.log(`MFA method: ${creds.mfa_method}`);
298
+ // Change MFA method
299
+ const result = await client.mfa.initiate({
300
+ userId: 'user@example.com',
301
+ mfaMethod: MfaMethod.EMAIL, // Current method for verification
302
+ mfaChangeTo: MfaMethod.TOTP, // New method to switch to
303
+ });
89
304
  ```
90
305
 
91
- **What it does automatically**: ECIES encryption, Ed25519 signatures, request/response crypto, Hawcx payload formatting, type-safe `MfaMethod` enum
306
+ **MFA Methods:**
307
+ ```typescript
308
+ enum MfaMethod {
309
+ EMAIL = 'email',
310
+ SMS = 'sms',
311
+ TOTP = 'totp',
312
+ }
313
+ ```
92
314
 
93
- ## Demo App
315
+ #### `client.mfa.verify(options): Promise<object>`
94
316
 
95
- A simple demo application is included to show the OAuth and delegation flows in action:
317
+ Verify MFA setup with OTP.
96
318
 
97
- ```bash
98
- cd demo
99
- cp env.template .env
100
- # Edit .env with your credentials
101
- npm install
102
- npm run dev
103
- # Visit http://localhost:3000
319
+ ```typescript
320
+ const result = await client.mfa.verify({
321
+ userId: 'user@example.com',
322
+ sessionId: 'session-from-initiate',
323
+ otp: '123456', // 6-digit code
324
+ });
104
325
  ```
105
326
 
106
- See [demo/README.md](demo/README.md) for details.
327
+ ### User Operations
107
328
 
108
- ## Configuration
329
+ #### `client.users.getCredentials(userId): Promise<object>`
109
330
 
110
- ### Environment Variables
331
+ Get user credentials/metadata.
111
332
 
112
- **For OAuth Code Exchange:**
113
- ```bash
114
- OAUTH_TOKEN_ENDPOINT="https://oauth.example.com/token"
115
- OAUTH_CLIENT_ID="your-client-id"
116
- OAUTH_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
117
- OAUTH_ISSUER="https://oauth.example.com" # Optional but recommended
118
- OAUTH_AUDIENCE="your-client-id" # Optional but recommended
333
+ ```typescript
334
+ const creds = await client.users.getCredentials('user@example.com');
335
+ console.log(creds.mfa_method);
119
336
  ```
120
337
 
121
- **For Hawcx Delegation:**
122
- ```bash
123
- SP_ED25519_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----..."
124
- SP_X25519_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----..."
125
- IDP_ED25519_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----..."
126
- IDP_X25519_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----..."
338
+ ### Device Operations
339
+
340
+ #### `client.devices.list(userId): Promise<object>`
341
+
342
+ List all devices for a user.
343
+
344
+ ```typescript
345
+ const { devices } = await client.devices.list('user@example.com');
127
346
  ```
128
347
 
129
- ### Public Key Formats
348
+ #### `client.devices.revoke(options): Promise<object>`
130
349
 
131
- The SDK accepts public keys in multiple formats:
350
+ Revoke a device (block it from authenticating).
132
351
 
133
- **From file:**
134
352
  ```typescript
135
- const claims = await exchangeCodeForClaims({
136
- // ...
137
- apiKey: process.env.HAWCX_API_KEY!,
138
- publicKey: '/path/to/public.pem' // Absolute path
353
+ await client.devices.revoke({
354
+ userId: 'user@example.com',
355
+ deviceId: 'device-h2index',
139
356
  });
140
357
  ```
141
358
 
142
- **From environment variable:**
359
+ #### `client.devices.unrevoke(options): Promise<object>`
360
+
361
+ Unrevoke a previously revoked device.
362
+
363
+ ```typescript
364
+ await client.devices.unrevoke({
365
+ userId: 'user@example.com',
366
+ deviceId: 'device-h2index',
367
+ });
368
+ ```
369
+
370
+ #### `client.devices.delete(options): Promise<object>`
371
+
372
+ Permanently delete a device.
373
+
143
374
  ```typescript
144
- const claims = await exchangeCodeForClaims({
145
- // ...
146
- apiKey: process.env.HAWCX_API_KEY!,
147
- publicKey: process.env.OAUTH_PUBLIC_KEY! // PEM string
375
+ await client.devices.delete({
376
+ userId: 'user@example.com',
377
+ deviceId: 'device-h2index',
148
378
  });
149
379
  ```
150
380
 
151
- **Expected PEM format:**
381
+ ### Generic Request
382
+
383
+ #### `client.request<TReq, TRes>(options): Promise<TRes>`
384
+
385
+ Send a custom encrypted request to any delegation endpoint.
386
+
387
+ ```typescript
388
+ const result = await client.request({
389
+ endpoint: '/hc_auth/v5/custom/endpoint',
390
+ payload: { userid: 'user@example.com', custom_field: 'value' },
391
+ headers: { 'X-Custom-Header': 'value' },
392
+ includeBaseUrl: true, // Optional (default: true)
393
+ });
152
394
  ```
153
- -----BEGIN PUBLIC KEY-----
154
- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
155
- -----END PUBLIC KEY-----
395
+
396
+ ### Credential Utilities
397
+
398
+ #### `parseHawcxSecretKey(secretKey): ParsedCredentials`
399
+
400
+ Parse a secret key into usable key material (advanced usage).
401
+
402
+ ```typescript
403
+ import { parseHawcxSecretKey } from '@hawcx/oauth-client';
404
+
405
+ const creds = parseHawcxSecretKey(process.env.HAWCX_SECRET_KEY!);
406
+ // Returns: { kid, hkid, signingKeyPem, verifyKeyPem, encryptKeyPem, decryptKeyPem }
156
407
  ```
157
408
 
158
- ## Error Handling
409
+ #### `generateCredentialBlob(options): string`
410
+
411
+ Generate a new credential blob (for admin tooling).
412
+
413
+ ```typescript
414
+ import { generateCredentialBlob } from '@hawcx/oauth-client';
415
+
416
+ const secretKey = generateCredentialBlob({
417
+ kid: 'customer-key-id',
418
+ hkid: 'hawcx-key-id',
419
+ edPrivate: Buffer.alloc(32), // Ed25519 private key (32 bytes)
420
+ xPrivate: Buffer.alloc(32), // X25519 private key (32 bytes)
421
+ hawcxEdPublic: Buffer.alloc(32), // Hawcx Ed25519 public key (32 bytes)
422
+ hawcxXPublic: Buffer.alloc(32), // Hawcx X25519 public key (32 bytes)
423
+ });
424
+ // Returns: "hwx_sk_v1_<base64url>"
425
+ ```
159
426
 
160
- The SDK provides specific exceptions for different failure scenarios:
427
+ ### Delegation Errors
161
428
 
162
429
  ```typescript
163
430
  import {
164
- exchangeCodeForClaims,
165
- OAuthExchangeError,
166
- JWTVerificationError,
167
- InvalidPublicKeyError
431
+ DelegationError, // Base delegation error
432
+ DelegationCryptoError, // Encryption/signing failed
433
+ DelegationRequestError, // Request to IDP failed
434
+ DelegationResponseError, // Invalid response from IDP
168
435
  } from '@hawcx/oauth-client';
169
436
 
170
437
  try {
171
- const claims = await exchangeCodeForClaims({
172
- code,
173
- oauthTokenUrl,
174
- clientId,
175
- apiKey,
176
- publicKey
177
- });
438
+ await client.mfa.initiate({ userId: 'user@example.com', mfaMethod: MfaMethod.EMAIL });
178
439
  } catch (error) {
179
- if (error instanceof OAuthExchangeError) {
180
- // Code exchange failed (invalid code, network error, etc.)
181
- console.error(`Exchange failed: ${error.message}`);
182
- console.error(`HTTP Status: ${error.statusCode}`);
183
- console.error(`Response: ${error.responseBody}`);
184
- } else if (error instanceof JWTVerificationError) {
185
- // JWT verification failed (invalid signature, expired, etc.)
186
- console.error(`Verification failed: ${error.message}`);
187
- console.error(`Original error: ${error.originalError}`);
188
- } else if (error instanceof InvalidPublicKeyError) {
189
- // Public key is invalid or unreadable
190
- console.error(`Key error: ${error.message}`);
440
+ if (error instanceof DelegationRequestError) {
441
+ console.log(error.statusCode); // HTTP status
442
+ console.log(error.responseBody); // Response body
191
443
  }
192
444
  }
193
445
  ```
194
446
 
195
- ### Common Error Scenarios
196
-
197
- | Exception | Common Causes | Recommended Action |
198
- |-----------|--------------|-------------------|
199
- | `OAuthExchangeError` | Invalid/expired code, network issues | Ask user to re-authenticate |
200
- | `JWTVerificationError` | Token tampering, expired token | Log incident, ask user to re-authenticate |
201
- | `InvalidPublicKeyError` | Wrong key, file not found | Check configuration, verify key format |
447
+ ---
202
448
 
203
- ## Security Best Practices
449
+ ## Step-Up Module
204
450
 
205
- ### โœ… DO
451
+ For step-up authentication flows (e.g., changing MFA method requires re-authentication).
206
452
 
207
- - **Always validate audience and issuer** in production environments
208
- - **Use HTTPS** for all OAuth endpoints
209
- - **Store keys securely** (environment variables, secrets manager)
210
- - **Set appropriate leeway** for clock skew (5-10 seconds typical)
211
- - **Log authentication failures** for security monitoring
212
- - **Rotate keys regularly** following your security policy
453
+ ### `StepUpClient`
213
454
 
214
- ### โŒ DON'T
455
+ ```typescript
456
+ import { StepUpClient } from '@hawcx/oauth-client';
457
+
458
+ const stepUp = StepUpClient.fromSecretKey({
459
+ baseUrl: 'https://api.hawcx.com',
460
+ secretKey: process.env.HAWCX_SECRET_KEY!,
461
+ relyingParty: 'your-app.com', // Required
462
+ apiKey: 'optional-api-key', // Optional
463
+ apiPrefix: '/v1', // Optional (default: '/v1')
464
+ timeoutSeconds: 15, // Optional
465
+ clockSkewSeconds: 300, // Optional
466
+ });
467
+ ```
215
468
 
216
- - **Never log JWT tokens** or claims containing sensitive data
217
- - **Don't disable signature verification** in production
218
- - **Don't use the id_token as your application's access token** (mint your own)
219
- - **Don't commit keys** to version control
220
- - **Don't ignore verification errors** or catch them silently
469
+ ### Methods
221
470
 
222
- ## API Reference
471
+ #### `stepUp.startToken(options): Promise<StepUpStartTokenResponse>`
223
472
 
224
- ### `exchangeCodeForClaims()`
473
+ Start a step-up authentication flow.
225
474
 
226
475
  ```typescript
227
- async function exchangeCodeForClaims(
228
- options: ExchangeCodeOptions
229
- ): Promise<Record<string, any>>
476
+ const { start_token, expires_in } = await stepUp.startToken({
477
+ userId: 'user@example.com',
478
+ purpose: 'change_mfa_method',
479
+ newMfaMethod: 'totp', // 'email_otp' | 'sms_otp' | 'totp'
480
+ });
481
+ // Send start_token to client for step-up authentication
230
482
  ```
231
483
 
232
- **Parameters:**
233
- - `code` (string): Authorization code from OAuth flow
234
- - `oauthTokenUrl` (string): Token endpoint URL
235
- - `clientId` (string): OAuth client identifier
236
- - `apiKey` (string): API key for Hawcx OAuth
237
- - `publicKey` (string, optional): RS256 public key (PEM string or file path) - optional if jwksUrl provided
238
- - `jwksUrl` (string, optional): JWKS URL for dynamic key fetching (alternative to publicKey)
239
- - `codeVerifier` (string, optional): PKCE code verifier (RFC 7636)
240
- - `redirectUri` (string, optional): OAuth redirect URI
241
- - `timeout` (number, optional): Request timeout in seconds (default: 20)
242
- - `audience` (string, optional): Expected 'aud' claim (recommended)
243
- - `issuer` (string, optional): Expected 'iss' claim (recommended)
244
- - `verifyExp` (boolean, optional): Verify token expiration (default: true)
245
- - `leeway` (number, optional): Clock skew tolerance in seconds (default: 0)
246
- - `algorithms` (string[], optional): Allowed signing algorithms (default: RS256, ES256/384/512, EdDSA)
484
+ #### `stepUp.consumeReceipt(options): Promise<StepUpConsumeResponse>`
247
485
 
248
- **Returns:** `Promise<Record<string, any>>` - Verified JWT claims
486
+ Consume a step-up receipt after user completes authentication.
249
487
 
250
- **Throws:**
251
- - `OAuthExchangeError`: Code exchange failed
252
- - `JWTVerificationError`: JWT verification failed
253
- - `InvalidPublicKeyError`: Public key invalid
488
+ ```typescript
489
+ const { ok } = await stepUp.consumeReceipt({
490
+ receipt: 'step-up-receipt-from-client',
491
+ });
492
+
493
+ if (ok) {
494
+ // Step-up verified, proceed with sensitive operation
495
+ }
496
+ ```
254
497
 
255
- ### `verifyJwt()`
498
+ ### Types
256
499
 
257
500
  ```typescript
258
- function verifyJwt(
259
- options: VerifyJwtOptions
260
- ): Record<string, any>
501
+ type StepUpPurpose = 'change_mfa_method';
502
+
503
+ type HxAuthMfaMethod = 'sms_otp' | 'email_otp' | 'totp';
504
+
505
+ interface StepUpStartTokenResponse {
506
+ start_token: string;
507
+ expires_in: number;
508
+ }
509
+
510
+ interface StepUpConsumeResponse {
511
+ ok: boolean;
512
+ }
261
513
  ```
262
514
 
263
- Verify a JWT and return its claims. See full documentation in [API.md](./API.md).
515
+ ---
516
+
517
+ ## Examples
518
+
519
+ ### Next.js Auth Callback
264
520
 
265
- ### HawcxDelegationClient
521
+ ```typescript
522
+ // app/api/auth/callback/route.ts
523
+ import { HawcxOAuth, TokenExchangeError } from '@hawcx/oauth-client';
524
+ import { cookies } from 'next/headers';
525
+
526
+ const oauth = new HawcxOAuth({
527
+ configId: process.env.HAWCX_CONFIG_ID!,
528
+ });
266
529
 
267
- See [DELEGATION.md](./DELEGATION.md) for full Hawcx delegation client documentation.
530
+ export async function GET(request: Request) {
531
+ const url = new URL(request.url);
532
+ const code = url.searchParams.get('code');
268
533
 
269
- ## TypeScript Support
534
+ const cookieStore = cookies();
535
+ const codeVerifier = cookieStore.get('code_verifier')?.value;
270
536
 
271
- This package is written in TypeScript and includes full type definitions:
537
+ if (!code || !codeVerifier) {
538
+ return Response.redirect('/login?error=missing_params');
539
+ }
540
+
541
+ try {
542
+ const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
543
+ // Create session with claims.sub, claims.email, etc.
544
+ return Response.redirect('/dashboard');
545
+ } catch (error) {
546
+ if (error instanceof TokenExchangeError) {
547
+ console.error('Token exchange failed:', error.message);
548
+ }
549
+ return Response.redirect('/login?error=auth_failed');
550
+ }
551
+ }
552
+ ```
553
+
554
+ ### Admin MFA Management
272
555
 
273
556
  ```typescript
274
- import type {
275
- ExchangeCodeOptions,
276
- VerifyJwtOptions
277
- } from '@hawcx/oauth-client';
557
+ import { DelegationClient, MfaMethod } from '@hawcx/oauth-client';
558
+
559
+ const admin = DelegationClient.fromSecretKey({
560
+ baseUrl: process.env.HAWCX_API_URL!,
561
+ secretKey: process.env.HAWCX_SECRET_KEY!,
562
+ });
563
+
564
+ // Reset user MFA to email
565
+ async function resetUserMfa(userId: string, otp: string) {
566
+ const initResult = await admin.mfa.initiate({
567
+ userId,
568
+ mfaMethod: MfaMethod.EMAIL,
569
+ });
570
+
571
+ const verifyResult = await admin.mfa.verify({
572
+ userId,
573
+ sessionId: initResult.session_id,
574
+ otp,
575
+ });
576
+
577
+ return verifyResult;
578
+ }
278
579
 
279
- // All options are fully typed
280
- const options: ExchangeCodeOptions = {
281
- code: 'abc123',
282
- oauthTokenUrl: 'https://oauth.example.com/token',
283
- clientId: 'my-app',
284
- publicKey: process.env.OAUTH_PUBLIC_KEY!,
285
- audience: 'my-app', // TypeScript will catch typos!
286
- };
580
+ // Revoke all user devices
581
+ async function revokeAllDevices(userId: string) {
582
+ const { devices } = await admin.devices.list(userId);
583
+
584
+ for (const device of devices) {
585
+ await admin.devices.revoke({
586
+ userId,
587
+ deviceId: device.h2index,
588
+ });
589
+ }
590
+ }
287
591
  ```
288
592
 
289
- ## Development
593
+ ### Step-Up for MFA Change
290
594
 
291
- ### Building
595
+ ```typescript
596
+ import { StepUpClient, DelegationClient, MfaMethod } from '@hawcx/oauth-client';
292
597
 
293
- ```bash
294
- npm install
295
- npm run build
598
+ const stepUp = StepUpClient.fromSecretKey({
599
+ baseUrl: process.env.HAWCX_API_URL!,
600
+ secretKey: process.env.HAWCX_SECRET_KEY!,
601
+ relyingParty: 'myapp.com',
602
+ });
603
+
604
+ const delegation = DelegationClient.fromSecretKey({
605
+ baseUrl: process.env.HAWCX_API_URL!,
606
+ secretKey: process.env.HAWCX_SECRET_KEY!,
607
+ });
608
+
609
+ async function changeMfaMethod(userId: string, newMethod: 'totp' | 'sms_otp' | 'email_otp') {
610
+ // 1. Start step-up flow
611
+ const { start_token } = await stepUp.startToken({
612
+ userId,
613
+ purpose: 'change_mfa_method',
614
+ newMfaMethod: newMethod,
615
+ });
616
+
617
+ // 2. Send start_token to client, user completes step-up auth
618
+ // 3. Client sends back receipt
619
+
620
+ // 4. Verify step-up receipt
621
+ const { ok } = await stepUp.consumeReceipt({ receipt: 'receipt-from-client' });
622
+
623
+ if (!ok) {
624
+ throw new Error('Step-up verification failed');
625
+ }
626
+
627
+ // 5. Now safe to change MFA
628
+ await delegation.mfa.initiate({
629
+ userId,
630
+ mfaMethod: MfaMethod.EMAIL,
631
+ mfaChangeTo: newMethod,
632
+ });
633
+ }
296
634
  ```
297
635
 
298
- ### Testing
636
+ ---
299
637
 
300
- ```bash
301
- npm test
302
- npm run test:coverage
638
+ ## Environment Variables
639
+
640
+ | Variable | Description |
641
+ |----------|-------------|
642
+ | `HAWCX_CONFIG_ID` | Your Hawcx OAuth config ID (for token verification) |
643
+ | `HAWCX_SECRET_KEY` | Your Hawcx secret key (for delegation/step-up APIs) |
644
+ | `HAWCX_API_URL` | API base URL (default: `https://api.hawcx.com`) |
645
+
646
+ ### Secret Key Format
647
+
648
+ The `HAWCX_SECRET_KEY` is a compact credential blob:
649
+
650
+ ```
651
+ hwx_sk_v1_<base64url-encoded-json>
303
652
  ```
304
653
 
305
- ### Linting
654
+ It contains:
655
+ - Your Ed25519 signing key (for request authentication)
656
+ - Your X25519 decryption key (for response decryption)
657
+ - Hawcx's Ed25519 public key (for response verification)
658
+ - Hawcx's X25519 public key (for request encryption)
659
+ - Key IDs for rotation tracking
306
660
 
307
- ```bash
308
- npm run lint
309
- npm run format
661
+ Generate via the Hawcx Dashboard or admin tools.
662
+
663
+ ---
664
+
665
+ ## JWKS Caching
666
+
667
+ The SDK automatically caches JWKS (JSON Web Key Set) for JWT verification:
668
+
669
+ - Keys are fetched lazily on first `verifyToken()` call
670
+ - Cached in memory for the lifetime of the `HawcxOAuth` instance
671
+ - Auto-refreshes if a key ID isn't found (handles key rotation)
672
+ - Call `refreshJwks()` to force a refresh (`clearCache()` is a deprecated alias โ€” see above)
673
+
674
+ ```typescript
675
+ // Keys fetched once, cached thereafter
676
+ const claims1 = await oauth.verifyToken(token1); // Fetches JWKS
677
+ const claims2 = await oauth.verifyToken(token2); // Uses cache
678
+ const claims3 = await oauth.verifyToken(token3); // Uses cache
679
+
680
+ // Force refresh if needed
681
+ oauth.refreshJwks();
682
+ const claims4 = await oauth.verifyToken(token4); // Fetches JWKS again
310
683
  ```
684
+
685
+ ---
686
+
687
+ ## Requirements
688
+
689
+ - Node.js >= 18.0.0
690
+
691
+ ## License
692
+
693
+ MIT