@angular-helpers/worker-http 0.2.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 +399 -0
- package/fesm2022/angular-helpers-worker-http-backend.mjs +3 -0
- package/fesm2022/angular-helpers-worker-http-crypto.mjs +146 -0
- package/fesm2022/angular-helpers-worker-http-interceptors.mjs +453 -0
- package/fesm2022/angular-helpers-worker-http-serializer.mjs +180 -0
- package/fesm2022/angular-helpers-worker-http-transport.mjs +116 -0
- package/fesm2022/angular-helpers-worker-http.mjs +20 -0
- package/package.json +81 -0
- package/types/angular-helpers-worker-http-backend.d.ts +42 -0
- package/types/angular-helpers-worker-http-crypto.d.ts +127 -0
- package/types/angular-helpers-worker-http-interceptors.d.ts +244 -0
- package/types/angular-helpers-worker-http-serializer.d.ts +89 -0
- package/types/angular-helpers-worker-http-transport.d.ts +115 -0
- package/types/angular-helpers-worker-http.d.ts +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
[Leer en Español](./README.es.md)
|
|
2
|
+
|
|
3
|
+
# @angular-helpers/worker-http
|
|
4
|
+
|
|
5
|
+
Move HTTP requests off the main thread. A composable toolkit for Angular that runs `fetch()` inside Web Workers, protecting your UI from network latency while adding security primitives (HMAC signing, content integrity, rate limiting) that live entirely in the worker's isolated scope.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why?
|
|
10
|
+
|
|
11
|
+
Every `HttpClient` request blocks a shared thread budget. When a slow API stalls, your animations stutter and user interactions queue up. Web Workers solve this at the architecture level: they run on a separate OS thread, so a 2-second network call costs the main thread nothing.
|
|
12
|
+
|
|
13
|
+
On top of that, workers provide a natural isolation boundary for security-sensitive logic: HMAC keys, request signing, and content verification never touch the main thread's memory.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Package map
|
|
18
|
+
|
|
19
|
+
| Entry point | Description | Status |
|
|
20
|
+
| ------------------------------------------- | ---------------------------------------------------------------- | -------------- |
|
|
21
|
+
| `@angular-helpers/worker-http/transport` | Typed RPC bridge, round-robin pool, cancellation | ✅ Available |
|
|
22
|
+
| `@angular-helpers/worker-http/serializer` | Pluggable serialization (structured clone, seroval, auto-detect) | ✅ Available |
|
|
23
|
+
| `@angular-helpers/worker-http/interceptors` | Pure-function interceptor pipeline for workers | ✅ Available |
|
|
24
|
+
| `@angular-helpers/worker-http/crypto` | WebCrypto primitives (HMAC, AES-GCM, SHA hashing) | ✅ Available |
|
|
25
|
+
| `@angular-helpers/worker-http/backend` | Angular `HttpBackend` replacement — `provideWorkerHttpClient()` | 🔧 In progress |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Architecture at a glance
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Main thread Web Worker
|
|
33
|
+
──────────────────────────── ──────────────────────────────────
|
|
34
|
+
Angular HttpClient createWorkerPipeline([
|
|
35
|
+
└─ WorkerHttpBackend loggingInterceptor(),
|
|
36
|
+
└─ WorkerTransport retryInterceptor({ maxRetries: 3 }),
|
|
37
|
+
└─ postMessage ───────► hmacSigningInterceptor({ keyMaterial }),
|
|
38
|
+
◄─────── cacheInterceptor({ ttl: 60000 }),
|
|
39
|
+
transfer ])
|
|
40
|
+
(zero-copy)
|
|
41
|
+
fetch() ──► API Server
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Entry points
|
|
47
|
+
|
|
48
|
+
### `/transport` — Typed RPC bridge
|
|
49
|
+
|
|
50
|
+
A framework-agnostic, type-safe bridge between the main thread and a Web Worker. Wraps `postMessage` with request/response correlation, Observable API, and automatic cancellation on unsubscribe.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { createWorkerTransport } from '@angular-helpers/worker-http/transport';
|
|
54
|
+
|
|
55
|
+
const transport = createWorkerTransport({
|
|
56
|
+
workerUrl: new URL('./workers/api.worker', import.meta.url),
|
|
57
|
+
maxInstances: 2,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Returns Observable — unsubscribe sends a cancel message to the worker
|
|
61
|
+
const response$ = transport.execute(request);
|
|
62
|
+
|
|
63
|
+
// Clean up
|
|
64
|
+
transport.terminate();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Features:**
|
|
68
|
+
|
|
69
|
+
- Round-robin pool (`maxInstances`) for parallel request handling
|
|
70
|
+
- Request cancellation via `AbortController` in the worker
|
|
71
|
+
- Automatic `Transferable` detection for zero-copy `ArrayBuffer` transfer
|
|
72
|
+
- Lazy worker instantiation — no worker created until first request
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### `/interceptors` — Worker-side pipeline
|
|
77
|
+
|
|
78
|
+
Pure-function interceptors that run inside the worker. No Angular DI, no DOM access — just `(req, next) => Promise<response>`.
|
|
79
|
+
|
|
80
|
+
#### Setup in your worker file
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// workers/secure.worker.ts
|
|
84
|
+
import { createWorkerPipeline } from '@angular-helpers/worker-http/interceptors';
|
|
85
|
+
import {
|
|
86
|
+
hmacSigningInterceptor,
|
|
87
|
+
retryInterceptor,
|
|
88
|
+
loggingInterceptor,
|
|
89
|
+
} from '@angular-helpers/worker-http/interceptors';
|
|
90
|
+
|
|
91
|
+
createWorkerPipeline([
|
|
92
|
+
loggingInterceptor(),
|
|
93
|
+
retryInterceptor({ maxRetries: 3, initialDelay: 500 }),
|
|
94
|
+
hmacSigningInterceptor({
|
|
95
|
+
keyMaterial: new TextEncoder().encode(self.HMAC_SECRET),
|
|
96
|
+
headerName: 'X-HMAC-Signature',
|
|
97
|
+
}),
|
|
98
|
+
]);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Available interceptors
|
|
102
|
+
|
|
103
|
+
##### `retryInterceptor(config?)`
|
|
104
|
+
|
|
105
|
+
Retries failed requests with exponential backoff. Respects the `Retry-After` header.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
retryInterceptor({
|
|
109
|
+
maxRetries: 3, // default: 3 (0 = disabled, returns response as-is)
|
|
110
|
+
initialDelay: 1000, // ms, default: 1000
|
|
111
|
+
backoffMultiplier: 2, // default: 2 → delays: 1s, 2s, 4s
|
|
112
|
+
retryStatusCodes: [408, 429, 500, 502, 503, 504], // default list
|
|
113
|
+
retryOnNetworkError: true, // retry on fetch() throws (default: true)
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
##### `cacheInterceptor(config?)`
|
|
118
|
+
|
|
119
|
+
In-worker response cache. State is per-factory-instance and resets when the worker is terminated.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
cacheInterceptor({
|
|
123
|
+
ttl: 60000, // ms, default: 60000 (1 min). 0 = never cache
|
|
124
|
+
maxEntries: 100, // default: 100. Eviction: FIFO (insertion order)
|
|
125
|
+
methods: ['GET'], // default: ['GET']
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
##### `hmacSigningInterceptor(config)`
|
|
130
|
+
|
|
131
|
+
Signs outgoing requests with HMAC-SHA256/384/512 via the native WebCrypto API. The `CryptoKey` is imported once per factory instance and reused across requests.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
hmacSigningInterceptor({
|
|
135
|
+
keyMaterial: rawKeyBytes, // ArrayBuffer | Uint8Array
|
|
136
|
+
algorithm: 'SHA-256', // default: 'SHA-256'
|
|
137
|
+
headerName: 'X-HMAC-Signature', // default: 'X-HMAC-Signature'
|
|
138
|
+
payloadBuilder: (
|
|
139
|
+
req, // optional: what to sign
|
|
140
|
+
) => `${req.method}:${req.url}:${JSON.stringify(req.body)}`,
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
##### `loggingInterceptor(config?)`
|
|
145
|
+
|
|
146
|
+
Logs request/response to `console.log` (or a custom logger). Logger exceptions are swallowed — a logging failure never interrupts the pipeline.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
loggingInterceptor({
|
|
150
|
+
logger: (msg, data) => myMonitoring.log(msg, data), // default: console.log
|
|
151
|
+
includeHeaders: false, // default: false
|
|
152
|
+
});
|
|
153
|
+
// Output: [worker] → GET https://api.example.com (0ms)
|
|
154
|
+
// [worker] ← 200 https://api.example.com (47ms)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
##### `rateLimitInterceptor(config?)`
|
|
158
|
+
|
|
159
|
+
Client-side sliding-window rate limiter. Throws `{ status: 429 }` when the limit is exceeded.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
rateLimitInterceptor({
|
|
163
|
+
maxRequests: 100, // default: 100
|
|
164
|
+
windowMs: 60000, // default: 60000 (1 min)
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
##### `contentIntegrityInterceptor(config?)`
|
|
169
|
+
|
|
170
|
+
Verifies the SHA-256 hash of the response body against a server-provided header. Useful when the server signs responses.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
contentIntegrityInterceptor({
|
|
174
|
+
algorithm: 'SHA-256', // default: 'SHA-256'
|
|
175
|
+
headerName: 'X-Content-Hash', // default: 'X-Content-Hash'
|
|
176
|
+
requireHash: false, // default: false. true = throw if header absent
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
##### `composeInterceptors(...fns)`
|
|
181
|
+
|
|
182
|
+
Composes multiple interceptors into a single `WorkerInterceptorFn`. Interceptors run left-to-right.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { composeInterceptors } from '@angular-helpers/worker-http/interceptors';
|
|
186
|
+
|
|
187
|
+
const securityLayer = composeInterceptors(
|
|
188
|
+
rateLimitInterceptor({ maxRequests: 50 }),
|
|
189
|
+
hmacSigningInterceptor({ keyMaterial }),
|
|
190
|
+
contentIntegrityInterceptor({ requireHash: true }),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
createWorkerPipeline([loggingInterceptor(), securityLayer]);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Custom interceptors
|
|
197
|
+
|
|
198
|
+
Implement `WorkerInterceptorFn` — a pure function with no external dependencies:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import type { WorkerInterceptorFn } from '@angular-helpers/worker-http/interceptors';
|
|
202
|
+
|
|
203
|
+
export const authTokenInterceptor: WorkerInterceptorFn = (req, next) => {
|
|
204
|
+
return next({
|
|
205
|
+
...req,
|
|
206
|
+
headers: { ...req.headers, Authorization: [`Bearer ${TOKEN}`] },
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### `/serializer` — Pluggable serialization
|
|
214
|
+
|
|
215
|
+
Handles the `postMessage` serialization boundary. Structured clone is zero-overhead but loses `Date`, `Map`, `Set` fidelity. `seroval` preserves full type fidelity. The auto-serializer picks the best strategy per payload.
|
|
216
|
+
|
|
217
|
+
#### `structuredCloneSerializer` (default)
|
|
218
|
+
|
|
219
|
+
Zero-overhead. Uses the browser's native structured clone algorithm. Best for simple objects and primitives.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { structuredCloneSerializer } from '@angular-helpers/worker-http/serializer';
|
|
223
|
+
// No setup needed — this is the default when no serializer is configured.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `createSerovalSerializer()` — Full type fidelity
|
|
227
|
+
|
|
228
|
+
Requires `seroval` as an optional peer dependency (`npm install seroval`).
|
|
229
|
+
|
|
230
|
+
Supports: `Date`, `Map`, `Set`, `BigInt`, `RegExp`, circular references, and more.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { createSerovalSerializer } from '@angular-helpers/worker-http/serializer';
|
|
234
|
+
|
|
235
|
+
// Factory is async — pre-loads the seroval module
|
|
236
|
+
const serializer = await createSerovalSerializer();
|
|
237
|
+
|
|
238
|
+
const payload = serializer.serialize({ date: new Date(), tags: new Set(['a', 'b']) });
|
|
239
|
+
const original = serializer.deserialize(payload);
|
|
240
|
+
// original.date instanceof Date → true
|
|
241
|
+
// original.tags instanceof Set → true
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### `createAutoSerializer()` — Smart auto-detection
|
|
245
|
+
|
|
246
|
+
Automatically picks the best strategy per payload. The factory is async (pre-loads `seroval` during initialization), but the returned serializer is fully synchronous.
|
|
247
|
+
|
|
248
|
+
**Detection logic (depth-1):**
|
|
249
|
+
|
|
250
|
+
- Contains `Date`, `Map`, `Set`, or `RegExp` at the top level or as direct array/object values → `seroval`
|
|
251
|
+
- Otherwise → structured clone (zero overhead)
|
|
252
|
+
|
|
253
|
+
Payloads larger than `transferThreshold` (default: 100 KiB) are encoded to `ArrayBuffer` and transferred zero-copy.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { createAutoSerializer } from '@angular-helpers/worker-http/serializer';
|
|
257
|
+
|
|
258
|
+
const auto = await createAutoSerializer({
|
|
259
|
+
transferThreshold: 102400, // bytes, default: 100 KiB
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Simple object → structured-clone (no overhead)
|
|
263
|
+
auto.serialize({ id: 1, name: 'Alice' }); // format: 'structured-clone'
|
|
264
|
+
|
|
265
|
+
// Object with Date → seroval (type fidelity)
|
|
266
|
+
auto.serialize({ createdAt: new Date() }); // format: 'seroval'
|
|
267
|
+
|
|
268
|
+
// Large payload → ArrayBuffer transfer (zero-copy)
|
|
269
|
+
auto.serialize(hugeDataset); // transferables: [ArrayBuffer]
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
> **Depth-1 limitation**: `[{ createdAt: new Date() }]` — the `Date` is inside a nested object; not detected at depth-1. For deeply nested complex types, use `createSerovalSerializer()` directly.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### `/crypto` — WebCrypto primitives
|
|
277
|
+
|
|
278
|
+
Standalone WebCrypto utilities. Useful in both workers and the main thread, but workers provide memory isolation for key material.
|
|
279
|
+
|
|
280
|
+
#### `createHmacSigner(config)`
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { createHmacSigner } from '@angular-helpers/worker-http/crypto';
|
|
284
|
+
|
|
285
|
+
const signer = await createHmacSigner({
|
|
286
|
+
keyMaterial: rawKeyBytes,
|
|
287
|
+
algorithm: 'SHA-256', // default
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const signature = await signer.sign('GET:/api/users:');
|
|
291
|
+
const isValid = await signer.verify('GET:/api/users:', signature);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `createAesEncryptor(config)`
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { createAesEncryptor } from '@angular-helpers/worker-http/crypto';
|
|
298
|
+
|
|
299
|
+
const encryptor = await createAesEncryptor({ keyLength: 256 });
|
|
300
|
+
|
|
301
|
+
const { ciphertext, iv } = await encryptor.encrypt('sensitive data');
|
|
302
|
+
const plaintext = await encryptor.decrypt(ciphertext, iv);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
#### `createContentHasher()`
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { createContentHasher } from '@angular-helpers/worker-http/crypto';
|
|
309
|
+
|
|
310
|
+
const hasher = createContentHasher();
|
|
311
|
+
const hash = await hasher.hash('SHA-256', data); // → hex string
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
### `/backend` — Angular `HttpBackend` replacement (in progress)
|
|
317
|
+
|
|
318
|
+
> 🔧 **This entry point is currently in development.**
|
|
319
|
+
|
|
320
|
+
The goal is a drop-in replacement for Angular's `HttpBackend` that transparently routes requests to the appropriate worker.
|
|
321
|
+
|
|
322
|
+
**Planned API:**
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// app.config.ts
|
|
326
|
+
import {
|
|
327
|
+
provideWorkerHttpClient,
|
|
328
|
+
withWorkerConfigs,
|
|
329
|
+
withWorkerRoutes,
|
|
330
|
+
withWorkerFallback,
|
|
331
|
+
} from '@angular-helpers/worker-http/backend';
|
|
332
|
+
|
|
333
|
+
bootstrapApplication(AppComponent, {
|
|
334
|
+
providers: [
|
|
335
|
+
provideWorkerHttpClient(
|
|
336
|
+
withWorkerConfigs([
|
|
337
|
+
{
|
|
338
|
+
id: 'secure',
|
|
339
|
+
workerUrl: new URL('./workers/secure.worker', import.meta.url),
|
|
340
|
+
maxInstances: 2,
|
|
341
|
+
},
|
|
342
|
+
]),
|
|
343
|
+
withWorkerRoutes([{ pattern: /\/api\/secure\//, worker: 'secure', priority: 10 }]),
|
|
344
|
+
withWorkerFallback('main-thread'), // SSR-safe fallback
|
|
345
|
+
),
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// data.service.ts — identical to normal HttpClient usage
|
|
350
|
+
export class DataService {
|
|
351
|
+
private http = inject(HttpClient);
|
|
352
|
+
|
|
353
|
+
getReports() {
|
|
354
|
+
return this.http.get<Report[]>('/api/secure/reports');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Design principles
|
|
362
|
+
|
|
363
|
+
- **Zero main-thread cost** — `fetch()` runs entirely in the worker; the main thread only handles the `postMessage` handoff
|
|
364
|
+
- **Black box** — developers use `HttpClient` as normal; workers are an implementation detail
|
|
365
|
+
- **Pure-function interceptors** — no Angular DI, no DOM, no closures over mutable state; fully testable without a browser
|
|
366
|
+
- **Composable** — use only the sub-packages you need; each is independently useful
|
|
367
|
+
- **SSR-safe** — `typeof Worker === 'undefined'` falls back to a standard `FetchBackend` automatically
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Serialization strategy decision guide
|
|
372
|
+
|
|
373
|
+
| Payload type | Recommended serializer | Reason |
|
|
374
|
+
| ------------------------------------ | -------------------------------------- | --------------------------- |
|
|
375
|
+
| Simple objects, arrays of primitives | `structuredCloneSerializer` (default) | Zero overhead |
|
|
376
|
+
| Objects with `Date`, `Map`, `Set` | `createSerovalSerializer()` | Full type fidelity |
|
|
377
|
+
| Unknown payload shape | `createAutoSerializer()` | Depth-1 auto-detect |
|
|
378
|
+
| Large arrays (> 100 KiB) | `createAutoSerializer()` | Auto ArrayBuffer transfer |
|
|
379
|
+
| Deeply nested complex types | `createSerovalSerializer()` explicitly | Auto-detect is depth-1 only |
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Browser support
|
|
384
|
+
|
|
385
|
+
All features require a browser that supports:
|
|
386
|
+
|
|
387
|
+
- **Web Workers** — all modern browsers (Chrome 4+, Firefox 3.5+, Safari 4+)
|
|
388
|
+
- **WebCrypto (`crypto.subtle`)** — requires HTTPS (or `localhost`)
|
|
389
|
+
- **`Transferable` objects** — `ArrayBuffer` transfer supported in all modern browsers
|
|
390
|
+
|
|
391
|
+
Server-Side Rendering (SSR) is supported via automatic fallback to the main thread.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Related documentation
|
|
396
|
+
|
|
397
|
+
- [Architecture & Feasibility Study](../../docs/sdd-angular-http-web-workers.md)
|
|
398
|
+
- [Deep Research: Serialization, Transferables, Benchmarks](../../docs/research/http-worker-deep-research.md)
|
|
399
|
+
- [Product Breakdown](../../docs/research/http-worker-product-breakdown.md)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
function toArrayBuffer$2(data) {
|
|
2
|
+
if (typeof data === 'string') {
|
|
3
|
+
return new TextEncoder().encode(data).buffer;
|
|
4
|
+
}
|
|
5
|
+
if (data instanceof Uint8Array) {
|
|
6
|
+
return data.buffer;
|
|
7
|
+
}
|
|
8
|
+
return data;
|
|
9
|
+
}
|
|
10
|
+
function arrayBufferToHex$1(buffer) {
|
|
11
|
+
return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates an HMAC signer using the Web Crypto API.
|
|
15
|
+
*
|
|
16
|
+
* Works in both main thread and web workers.
|
|
17
|
+
* Requires a secure context (HTTPS).
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const signer = await createHmacSigner({
|
|
22
|
+
* keyMaterial: new TextEncoder().encode('my-secret-key'),
|
|
23
|
+
* algorithm: 'SHA-256',
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* const signature = await signer.signHex('request-payload');
|
|
27
|
+
* const isValid = await signer.verify('request-payload', signatureBuffer);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
async function createHmacSigner(config) {
|
|
31
|
+
const algorithm = config.algorithm ?? 'SHA-256';
|
|
32
|
+
const keyMaterial = config.keyMaterial instanceof Uint8Array
|
|
33
|
+
? new Uint8Array(config.keyMaterial.buffer.slice(0))
|
|
34
|
+
: new Uint8Array(config.keyMaterial);
|
|
35
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyMaterial, { name: 'HMAC', hash: algorithm }, false, ['sign', 'verify']);
|
|
36
|
+
return {
|
|
37
|
+
async sign(data) {
|
|
38
|
+
return crypto.subtle.sign('HMAC', cryptoKey, toArrayBuffer$2(data));
|
|
39
|
+
},
|
|
40
|
+
async signHex(data) {
|
|
41
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, toArrayBuffer$2(data));
|
|
42
|
+
return arrayBufferToHex$1(signature);
|
|
43
|
+
},
|
|
44
|
+
async verify(data, signature) {
|
|
45
|
+
return crypto.subtle.verify('HMAC', cryptoKey, signature, toArrayBuffer$2(data));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toArrayBuffer$1(data) {
|
|
51
|
+
if (typeof data === 'string') {
|
|
52
|
+
return new TextEncoder().encode(data).buffer;
|
|
53
|
+
}
|
|
54
|
+
if (data instanceof Uint8Array) {
|
|
55
|
+
return data.buffer;
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates an AES encryptor using the Web Crypto API.
|
|
61
|
+
*
|
|
62
|
+
* Works in both main thread and web workers.
|
|
63
|
+
* Requires a secure context (HTTPS).
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const encryptor = await createAesEncryptor({
|
|
68
|
+
* keyMaterial: new TextEncoder().encode('32-byte-secret-key-for-aes-256!'),
|
|
69
|
+
* algorithm: 'AES-GCM',
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* const encrypted = await encryptor.encrypt('sensitive data');
|
|
73
|
+
* const decrypted = await encryptor.decrypt(encrypted);
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
async function createAesEncryptor(config) {
|
|
77
|
+
const algorithm = config.algorithm ?? 'AES-GCM';
|
|
78
|
+
const keyLength = config.keyLength ?? 256;
|
|
79
|
+
const keyMaterial = config.keyMaterial instanceof Uint8Array
|
|
80
|
+
? new Uint8Array(config.keyMaterial.buffer.slice(0))
|
|
81
|
+
: new Uint8Array(config.keyMaterial);
|
|
82
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyMaterial, { name: algorithm, length: keyLength }, false, ['encrypt', 'decrypt']);
|
|
83
|
+
return {
|
|
84
|
+
async encrypt(data) {
|
|
85
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
86
|
+
const params = algorithm === 'AES-GCM'
|
|
87
|
+
? { name: algorithm, iv }
|
|
88
|
+
: algorithm === 'AES-CBC'
|
|
89
|
+
? { name: algorithm, iv }
|
|
90
|
+
: { name: algorithm, counter: iv, length: 64 };
|
|
91
|
+
const ciphertext = await crypto.subtle.encrypt(params, cryptoKey, toArrayBuffer$1(data));
|
|
92
|
+
return { ciphertext, iv, algorithm };
|
|
93
|
+
},
|
|
94
|
+
async decrypt(payload) {
|
|
95
|
+
const iv = payload.iv;
|
|
96
|
+
const params = payload.algorithm === 'AES-GCM'
|
|
97
|
+
? { name: payload.algorithm, iv }
|
|
98
|
+
: payload.algorithm === 'AES-CBC'
|
|
99
|
+
? { name: payload.algorithm, iv }
|
|
100
|
+
: { name: payload.algorithm, counter: iv, length: 64 };
|
|
101
|
+
return crypto.subtle.decrypt(params, cryptoKey, payload.ciphertext);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toArrayBuffer(data) {
|
|
107
|
+
if (typeof data === 'string') {
|
|
108
|
+
return new TextEncoder().encode(data).buffer;
|
|
109
|
+
}
|
|
110
|
+
if (data instanceof Uint8Array) {
|
|
111
|
+
return data.buffer;
|
|
112
|
+
}
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
function arrayBufferToHex(buffer) {
|
|
116
|
+
return [...new Uint8Array(buffer)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Creates a content hasher using the Web Crypto API.
|
|
120
|
+
*
|
|
121
|
+
* Works in both main thread and web workers.
|
|
122
|
+
* Useful for content integrity verification (e.g., response body hashing).
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const hasher = createContentHasher('SHA-256');
|
|
127
|
+
* const hex = await hasher.hashHex('response-body-content');
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
function createContentHasher(algorithm = 'SHA-256') {
|
|
131
|
+
return {
|
|
132
|
+
async hash(data) {
|
|
133
|
+
return crypto.subtle.digest(algorithm, toArrayBuffer(data));
|
|
134
|
+
},
|
|
135
|
+
async hashHex(data) {
|
|
136
|
+
const hash = await crypto.subtle.digest(algorithm, toArrayBuffer(data));
|
|
137
|
+
return arrayBufferToHex(hash);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generated bundle index. Do not edit.
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
export { createAesEncryptor, createContentHasher, createHmacSigner };
|