@richardmcquiston01/error-log-adapter 0.5.0 → 0.7.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 +67 -19
- package/dist/index.cjs +237 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +132 -2
- package/dist/index.d.ts +132 -2
- package/dist/index.mjs +235 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,24 +70,26 @@ Constructor options are checked when the adapter is created and throw a descript
|
|
|
70
70
|
|
|
71
71
|
Every adapter takes an optional `prefix` (default `[error-log-adapter]`) that is printed before its failure messages.
|
|
72
72
|
|
|
73
|
-
| Adapter | Sink | Runs in | Main options
|
|
74
|
-
| ----------------------------------- | --------------------------------------------------------- | ------------- |
|
|
75
|
-
| `ConsoleErrorAdapter` | `console.error` | Anywhere |
|
|
76
|
-
| `WebhookErrorAdapter(url)` | HTTP `POST` of the entry as JSON | Anywhere | `headers`, `timeoutMs`, `allowInsecure`
|
|
77
|
-
| `MongoDbErrorAdapter(collection)` | Anything with `insertOne`, such as a MongoDB `Collection` | Node | `timeoutMs`
|
|
78
|
-
| `PostgresErrorAdapter(client)` | A Postgres table, through `pg` | Node | `tableName`, `createTable`, `timeoutMs`
|
|
79
|
-
| `MySqlErrorAdapter(client)` | A MySQL / MariaDB table, through `mysql2/promise` | Node | `tableName`, `createTable`, `timeoutMs`
|
|
80
|
-
| `SqliteErrorAdapter(client)` | A SQLite table, through any driver | Node, Bun | `tableName`, `createTable`, `timeoutMs`
|
|
81
|
-
| `SupabaseErrorAdapter(client)` | A Supabase table, through `supabase-js` | Anywhere | `tableName`, `timeoutMs`
|
|
82
|
-
| `RedisErrorAdapter(client)` | A Redis list or stream | Node | `mode`, `key`, `maxEntries`, `side`, `timeoutMs`
|
|
83
|
-
| `S3ErrorAdapter(client, bucket)` | S3, DigitalOcean Spaces, R2, MinIO objects | Node | `keyPrefix`, `batch`, `contentType`, `timeoutMs`
|
|
84
|
-
| `FirestoreErrorAdapter(collection)` | A Firestore collection | Node, browser | `timeoutMs`
|
|
85
|
-
| `RealtimeDatabaseErrorAdapter(ref)` | A Realtime Database reference | Node, browser | `timeoutMs`
|
|
86
|
-
| `FlatFileErrorAdapter(path)` | One text line per entry | Node, Bun | `formatLine`, `maxBytes`, `maxFiles`
|
|
87
|
-
| `JsonFileErrorAdapter(path)` | NDJSON lines or one JSON array | Node, Bun | `mode`, `maxEntries`, `indent`
|
|
88
|
-
| `IndexedDbErrorAdapter(database)` | An IndexedDB object store | Browser | `storeName`, `timeoutMs`
|
|
89
|
-
| `WebStorageErrorAdapter(storage)` | `sessionStorage` or `localStorage`, as one capped array | Browser | `key`, `maxEntries`, `locks`
|
|
90
|
-
| `
|
|
73
|
+
| Adapter | Sink | Runs in | Main options |
|
|
74
|
+
| ----------------------------------- | --------------------------------------------------------- | ------------- | ------------------------------------------------------------------- |
|
|
75
|
+
| `ConsoleErrorAdapter` | `console.error` | Anywhere | |
|
|
76
|
+
| `WebhookErrorAdapter(url)` | HTTP `POST` of the entry as JSON | Anywhere | `headers`, `timeoutMs`, `retries`, `signingSecret`, `allowInsecure` |
|
|
77
|
+
| `MongoDbErrorAdapter(collection)` | Anything with `insertOne`, such as a MongoDB `Collection` | Node | `timeoutMs` |
|
|
78
|
+
| `PostgresErrorAdapter(client)` | A Postgres table, through `pg` | Node | `tableName`, `createTable`, `timeoutMs` |
|
|
79
|
+
| `MySqlErrorAdapter(client)` | A MySQL / MariaDB table, through `mysql2/promise` | Node | `tableName`, `createTable`, `timeoutMs` |
|
|
80
|
+
| `SqliteErrorAdapter(client)` | A SQLite table, through any driver | Node, Bun | `tableName`, `createTable`, `timeoutMs` |
|
|
81
|
+
| `SupabaseErrorAdapter(client)` | A Supabase table, through `supabase-js` | Anywhere | `tableName`, `timeoutMs` |
|
|
82
|
+
| `RedisErrorAdapter(client)` | A Redis list or stream | Node | `mode`, `key`, `maxEntries`, `side`, `timeoutMs` |
|
|
83
|
+
| `S3ErrorAdapter(client, bucket)` | S3, DigitalOcean Spaces, R2, MinIO objects | Node | `keyPrefix`, `batch`, `contentType`, `timeoutMs` |
|
|
84
|
+
| `FirestoreErrorAdapter(collection)` | A Firestore collection | Node, browser | `timeoutMs` |
|
|
85
|
+
| `RealtimeDatabaseErrorAdapter(ref)` | A Realtime Database reference | Node, browser | `timeoutMs` |
|
|
86
|
+
| `FlatFileErrorAdapter(path)` | One text line per entry | Node, Bun | `formatLine`, `maxBytes`, `maxFiles` |
|
|
87
|
+
| `JsonFileErrorAdapter(path)` | NDJSON lines or one JSON array | Node, Bun | `mode`, `maxEntries`, `indent` |
|
|
88
|
+
| `IndexedDbErrorAdapter(database)` | An IndexedDB object store | Browser | `storeName`, `timeoutMs` |
|
|
89
|
+
| `WebStorageErrorAdapter(storage)` | `sessionStorage` or `localStorage`, as one capped array | Browser | `key`, `maxEntries`, `locks` |
|
|
90
|
+
| `EventEmitterErrorAdapter(emitter)` | An event on an emitter you own (in-process hooks) | Anywhere | `eventName`, `requireListener` |
|
|
91
|
+
| `WebSocketErrorAdapter(socket)` | One JSON message over a socket you own | Anywhere | (none) |
|
|
92
|
+
| `CompositeErrorAdapter([...])` | Several adapters at once; succeeds only if all succeed | Anywhere | `timeoutMs` |
|
|
91
93
|
|
|
92
94
|
Every stored record has the same shape: a UUID `id`, `code`, `message`, `timestamp` (ISO 8601), and optional `source` and `cause`. An `Error` cause is stored as `{ name, message, stack }`. `serializeEntry` and `serializeCause` are exported if you build your own sink.
|
|
93
95
|
|
|
@@ -99,7 +101,9 @@ What changes durability or size, by adapter:
|
|
|
99
101
|
- **Files** (`maxBytes` / `maxFiles`, `maxEntries`): the flat file adapter rotates by size; the JSON file adapter's `array` mode keeps the newest `maxEntries`.
|
|
100
102
|
- **Redis and Web Storage** (`maxEntries`): the oldest entries are dropped past the cap.
|
|
101
103
|
- **Browser storage**: `sessionStorage` belongs to one tab. `localStorage` is shared by every tab and has no atomic update, so two tabs logging at the same instant can overwrite each other's entry. `WebStorageErrorAdapter` takes a `navigator.locks` lock around each write, which reduces the loss but cannot remove it, since tabs in separate processes can still read a stale copy. Use `IndexedDbErrorAdapter` when every entry matters. Create its object store from your `onupgradeneeded` handler with `createErrorLogStore`. A stored value the adapter cannot read is copied to `<key>.corrupt` before it is replaced.
|
|
102
|
-
- **
|
|
104
|
+
- **Event emitter**: each entry is emitted as an `error-log` event whose payload is the stored record, for in-process hooks such as a toast, a metrics counter or a test assertion. Listeners run synchronously, so one that throws makes `log()` return `ok: false` with its message. Node's `EventEmitter` and mitt work as they are; a browser `EventTarget` needs a one-line wrapper (shown in the example below). Don't name the event `error` on a Node `EventEmitter`: it throws when nobody listens. Node's `emit` returns `false` when there are no listeners; pass `requireListener: true` to report that as a failure instead of letting the entry vanish.
|
|
105
|
+
- **WebSocket**: each entry is sent as one JSON text message (the serialized entry plus its UUID `id`) over a socket you own; the adapter never connects, reconnects or buffers. If the socket's `readyState` is not open (connecting, closing or closed), `log()` returns `ok: false` and says which. `ok: true` means the socket accepted the message, not that the server got it, since a WebSocket has no acknowledgement. Browsers, Node's global `WebSocket`, Deno, Bun and `ws` all fit. Pair it with a durable adapter in a `CompositeErrorAdapter` if entries must survive the socket being down.
|
|
106
|
+
- **Webhook**: the URL must be `https://` (or `http://localhost`, `127.0.0.1`, `[::1]`) unless you pass `allowInsecure: true`, because entries carry messages and stack traces. Redirects are not followed, since a redirect would turn the `POST` into a body-less `GET` or re-send the entry and your headers to another host; a redirect is reported as `ok: false`. Failure messages contain only the origin, never the path or query, because webhook URLs often hold a secret. Set `retries` to try again after a network error, a timeout, HTTP 408, 425, 429 or 5xx (exponential backoff with jitter from `retryDelayMs`, capped by `maxRetryDelayMs`, honoring `Retry-After`; other 4xx answers are never retried). With `retries` set, every attempt sends the same body including an `id`, so the receiver can drop a repeat. Set `signingSecret` to add `X-Signature-Timestamp` and `X-Signature-256` (`sha256=` + hex HMAC-SHA256 of `<timestamp>.<body>`); the receiver recomputes it and rejects old timestamps.
|
|
103
107
|
|
|
104
108
|
### When a sink is slow or down
|
|
105
109
|
|
|
@@ -209,6 +213,50 @@ const errorLog = new S3ErrorAdapter(s3, 'my-space', {
|
|
|
209
213
|
process.on('beforeExit', () => void errorLog.flush());
|
|
210
214
|
```
|
|
211
215
|
|
|
216
|
+
Show a toast, count errors, and keep a file copy, all from one `log` call:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { EventEmitter } from 'node:events';
|
|
220
|
+
import {
|
|
221
|
+
CompositeErrorAdapter,
|
|
222
|
+
EventEmitterErrorAdapter,
|
|
223
|
+
FlatFileErrorAdapter,
|
|
224
|
+
} from '@richardmcquiston01/error-log-adapter';
|
|
225
|
+
|
|
226
|
+
const bus = new EventEmitter();
|
|
227
|
+
bus.on('error-log', (record) => showToast(record.message));
|
|
228
|
+
bus.on('error-log', (record) => metrics.increment(`errors.${record.code}`));
|
|
229
|
+
|
|
230
|
+
const errorLog = new CompositeErrorAdapter([
|
|
231
|
+
new EventEmitterErrorAdapter(bus),
|
|
232
|
+
new FlatFileErrorAdapter('logs/errors.log'),
|
|
233
|
+
]);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
In a browser, wrap an `EventTarget`:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const target = new EventTarget();
|
|
240
|
+
const emitter = {
|
|
241
|
+
emit: (name: string, payload: unknown) =>
|
|
242
|
+
target.dispatchEvent(new CustomEvent(name, { detail: payload })),
|
|
243
|
+
};
|
|
244
|
+
target.addEventListener('error-log', (event) =>
|
|
245
|
+
console.log((event as CustomEvent).detail)
|
|
246
|
+
);
|
|
247
|
+
const errorLog = new EventEmitterErrorAdapter(emitter);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Send errors over a socket you manage, with a file as the fallback while it is down:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const socket = new WebSocket('wss://logs.example.com/errors');
|
|
254
|
+
const errorLog = new CompositeErrorAdapter([
|
|
255
|
+
new WebSocketErrorAdapter(socket),
|
|
256
|
+
new FlatFileErrorAdapter('./errors.log'),
|
|
257
|
+
]);
|
|
258
|
+
```
|
|
259
|
+
|
|
212
260
|
Write your own adapter by implementing the interface. Catch failures and return them rather than throwing:
|
|
213
261
|
|
|
214
262
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CompositeErrorAdapter: () => CompositeErrorAdapter,
|
|
24
24
|
ConsoleErrorAdapter: () => ConsoleErrorAdapter,
|
|
25
|
+
EventEmitterErrorAdapter: () => EventEmitterErrorAdapter,
|
|
25
26
|
FirestoreErrorAdapter: () => FirestoreErrorAdapter,
|
|
26
27
|
FlatFileErrorAdapter: () => FlatFileErrorAdapter,
|
|
27
28
|
IndexedDbErrorAdapter: () => IndexedDbErrorAdapter,
|
|
@@ -34,6 +35,7 @@ __export(index_exports, {
|
|
|
34
35
|
S3ErrorAdapter: () => S3ErrorAdapter,
|
|
35
36
|
SqliteErrorAdapter: () => SqliteErrorAdapter,
|
|
36
37
|
SupabaseErrorAdapter: () => SupabaseErrorAdapter,
|
|
38
|
+
WebSocketErrorAdapter: () => WebSocketErrorAdapter,
|
|
37
39
|
WebStorageErrorAdapter: () => WebStorageErrorAdapter,
|
|
38
40
|
WebhookErrorAdapter: () => WebhookErrorAdapter,
|
|
39
41
|
buildCreateErrorLogTableSql: () => buildCreateErrorLogTableSql,
|
|
@@ -278,6 +280,78 @@ function withTimeout(start, timeoutMs, message) {
|
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
// src/adapters/webhook-error-adapter.ts
|
|
283
|
+
var DEFAULT_RETRY_DELAY_MS = 500;
|
|
284
|
+
var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
|
|
285
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
286
|
+
var SigningUnavailableError = class extends Error {
|
|
287
|
+
};
|
|
288
|
+
async function hmacSha256Hex(secret, message) {
|
|
289
|
+
const subtle = globalThis.crypto?.subtle;
|
|
290
|
+
if (subtle === void 0) {
|
|
291
|
+
throw new SigningUnavailableError(
|
|
292
|
+
"signingSecret needs Web Crypto (crypto.subtle), which this runtime does not provide, so the entry was not sent unsigned"
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const encoder = new TextEncoder();
|
|
296
|
+
const key = await subtle.importKey(
|
|
297
|
+
"raw",
|
|
298
|
+
encoder.encode(secret),
|
|
299
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
300
|
+
false,
|
|
301
|
+
["sign"]
|
|
302
|
+
);
|
|
303
|
+
const signature = await subtle.sign(
|
|
304
|
+
"HMAC",
|
|
305
|
+
key,
|
|
306
|
+
encoder.encode(message)
|
|
307
|
+
);
|
|
308
|
+
return Array.from(
|
|
309
|
+
new Uint8Array(signature),
|
|
310
|
+
(byte) => byte.toString(16).padStart(2, "0")
|
|
311
|
+
).join("");
|
|
312
|
+
}
|
|
313
|
+
function parseRetryAfterMs(value) {
|
|
314
|
+
if (value === null || value === void 0 || value.trim() === "") {
|
|
315
|
+
return void 0;
|
|
316
|
+
}
|
|
317
|
+
const seconds = Number(value);
|
|
318
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
319
|
+
return seconds * 1e3;
|
|
320
|
+
}
|
|
321
|
+
const date = Date.parse(value);
|
|
322
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
323
|
+
}
|
|
324
|
+
function validateRetryOptions(options) {
|
|
325
|
+
const retries = options.retries ?? 0;
|
|
326
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
327
|
+
throw new RangeError(
|
|
328
|
+
`WebhookErrorAdapter: retries must be a whole number of 0 or more, received ${String(options.retries)}.`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
332
|
+
if (!Number.isFinite(retryDelayMs) || retryDelayMs <= 0) {
|
|
333
|
+
throw new RangeError(
|
|
334
|
+
`WebhookErrorAdapter: retryDelayMs must be a positive number, received ${String(options.retryDelayMs)}.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
338
|
+
if (!Number.isFinite(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs) {
|
|
339
|
+
throw new RangeError(
|
|
340
|
+
`WebhookErrorAdapter: maxRetryDelayMs must be a number no smaller than retryDelayMs (${retryDelayMs}), received ${String(options.maxRetryDelayMs)}.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (options.signingSecret === "") {
|
|
344
|
+
throw new RangeError(
|
|
345
|
+
"WebhookErrorAdapter: signingSecret must not be empty; leave it out to send unsigned."
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return { retries, retryDelayMs, maxRetryDelayMs };
|
|
349
|
+
}
|
|
350
|
+
function sleep(ms) {
|
|
351
|
+
return new Promise((resolve) => {
|
|
352
|
+
setTimeout(resolve, ms);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
281
355
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
282
356
|
"localhost",
|
|
283
357
|
"127.0.0.1",
|
|
@@ -303,15 +377,57 @@ function parseWebhookUrl(url, allowInsecure) {
|
|
|
303
377
|
);
|
|
304
378
|
}
|
|
305
379
|
var WebhookErrorAdapter = class {
|
|
306
|
-
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or
|
|
380
|
+
/** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or a timeout, retry or signing option is invalid. */
|
|
307
381
|
constructor(url, options = {}) {
|
|
308
382
|
this.url = url;
|
|
309
383
|
this.options = options;
|
|
310
384
|
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
311
385
|
this.timeoutMs = resolveTimeoutMs(options.timeoutMs, "WebhookErrorAdapter");
|
|
386
|
+
({
|
|
387
|
+
retries: this.retries,
|
|
388
|
+
retryDelayMs: this.retryDelayMs,
|
|
389
|
+
maxRetryDelayMs: this.maxRetryDelayMs
|
|
390
|
+
} = validateRetryOptions(options));
|
|
312
391
|
this.target = parseWebhookUrl(url, options.allowInsecure ?? false).origin;
|
|
313
392
|
}
|
|
314
393
|
async log(entry) {
|
|
394
|
+
let body;
|
|
395
|
+
try {
|
|
396
|
+
body = JSON.stringify(
|
|
397
|
+
hasCallerId(entry) || this.retries > 0 ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
|
|
398
|
+
);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
401
|
+
return this.fail(
|
|
402
|
+
`could not prepare the entry: ${this.redact(describeError(error))}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
let attempt = await this.send(body);
|
|
406
|
+
let attempts = 1;
|
|
407
|
+
while (!attempt.ok && attempt.retryable && attempts <= this.retries) {
|
|
408
|
+
await sleep(this.retryWaitMs(attempts, attempt.retryAfterMs));
|
|
409
|
+
attempt = await this.send(body);
|
|
410
|
+
attempts += 1;
|
|
411
|
+
}
|
|
412
|
+
if (attempt.ok) {
|
|
413
|
+
return { ok: true };
|
|
414
|
+
}
|
|
415
|
+
return this.fail(
|
|
416
|
+
attempts > 1 ? `${attempt.reason} (after ${attempts} attempts)` : attempt.reason
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
/** Milliseconds to wait before retry number `retryNumber` (1 for the first). */
|
|
420
|
+
retryWaitMs(retryNumber, retryAfterMs) {
|
|
421
|
+
if (retryAfterMs !== void 0) {
|
|
422
|
+
return Math.min(retryAfterMs, this.maxRetryDelayMs);
|
|
423
|
+
}
|
|
424
|
+
const ceiling = Math.min(
|
|
425
|
+
this.maxRetryDelayMs,
|
|
426
|
+
this.retryDelayMs * 2 ** (retryNumber - 1)
|
|
427
|
+
);
|
|
428
|
+
return ceiling / 2 + Math.random() * ceiling / 2;
|
|
429
|
+
}
|
|
430
|
+
async send(body) {
|
|
315
431
|
const controller = new AbortController();
|
|
316
432
|
let timedOut = false;
|
|
317
433
|
const timer = this.timeoutMs === Infinity ? void 0 : setTimeout(() => {
|
|
@@ -323,43 +439,70 @@ var WebhookErrorAdapter = class {
|
|
|
323
439
|
method: "POST",
|
|
324
440
|
headers: {
|
|
325
441
|
"Content-Type": "application/json",
|
|
326
|
-
...this.options.headers
|
|
442
|
+
...this.options.headers,
|
|
443
|
+
...await this.signatureHeaders(body)
|
|
327
444
|
},
|
|
328
|
-
|
|
329
|
-
body: JSON.stringify(
|
|
330
|
-
hasCallerId(entry) ? { id: resolveEntryId(entry), ...serializeEntry(entry) } : serializeEntry(entry)
|
|
331
|
-
),
|
|
445
|
+
body,
|
|
332
446
|
// A redirect would turn the POST into a GET without a body (302, 303)
|
|
333
447
|
// or re-send the entry and headers to another host (307, 308).
|
|
334
448
|
redirect: "manual",
|
|
335
449
|
signal: controller.signal
|
|
336
450
|
});
|
|
337
451
|
if (response.type === "opaqueredirect") {
|
|
338
|
-
return this.
|
|
452
|
+
return this.rejected(this.redirectMessage("a redirect"));
|
|
339
453
|
}
|
|
340
454
|
if (!response.ok) {
|
|
341
455
|
if (response.status >= 300 && response.status < 400) {
|
|
342
|
-
return this.
|
|
456
|
+
return this.rejected(this.redirectMessage(`HTTP ${response.status}`));
|
|
343
457
|
}
|
|
344
|
-
return
|
|
345
|
-
|
|
346
|
-
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
reason: `webhook ${this.target} responded with HTTP ${response.status}`,
|
|
461
|
+
retryable: response.status >= 500 || RETRYABLE_STATUSES.has(response.status),
|
|
462
|
+
retryAfterMs: parseRetryAfterMs(response.headers?.get("retry-after"))
|
|
463
|
+
};
|
|
347
464
|
}
|
|
348
465
|
return { ok: true };
|
|
349
466
|
} catch (error) {
|
|
350
467
|
console.error(`${this.prefix} WebhookErrorAdapter:`, error);
|
|
468
|
+
if (error instanceof SigningUnavailableError) {
|
|
469
|
+
return this.rejected(error.message);
|
|
470
|
+
}
|
|
351
471
|
if (timedOut) {
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
|
|
472
|
+
return {
|
|
473
|
+
ok: false,
|
|
474
|
+
reason: timeoutMessage(`webhook ${this.target}`, this.timeoutMs),
|
|
475
|
+
retryable: true
|
|
476
|
+
};
|
|
355
477
|
}
|
|
356
|
-
return
|
|
357
|
-
|
|
358
|
-
|
|
478
|
+
return {
|
|
479
|
+
ok: false,
|
|
480
|
+
reason: `could not reach webhook ${this.target}: ${this.redact(describeError(error))}`,
|
|
481
|
+
retryable: true
|
|
482
|
+
};
|
|
359
483
|
} finally {
|
|
360
484
|
clearTimeout(timer);
|
|
361
485
|
}
|
|
362
486
|
}
|
|
487
|
+
/** Timestamp and signature headers, or none when no `signingSecret` is set. */
|
|
488
|
+
async signatureHeaders(body) {
|
|
489
|
+
const secret = this.options.signingSecret;
|
|
490
|
+
if (secret === void 0) {
|
|
491
|
+
return {};
|
|
492
|
+
}
|
|
493
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
494
|
+
const signature = await hmacSha256Hex(
|
|
495
|
+
secret,
|
|
496
|
+
`${timestamp}.${body}`
|
|
497
|
+
);
|
|
498
|
+
return {
|
|
499
|
+
"X-Signature-Timestamp": timestamp,
|
|
500
|
+
"X-Signature-256": `sha256=${signature}`
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
rejected(reason) {
|
|
504
|
+
return { ok: false, reason, retryable: false };
|
|
505
|
+
}
|
|
363
506
|
redirectMessage(what) {
|
|
364
507
|
return `webhook ${this.target} responded with ${what}, which is not followed because the entry would not arrive intact; use the final URL instead`;
|
|
365
508
|
}
|
|
@@ -1639,10 +1782,86 @@ var SupabaseErrorAdapter = class {
|
|
|
1639
1782
|
});
|
|
1640
1783
|
}
|
|
1641
1784
|
};
|
|
1785
|
+
|
|
1786
|
+
// src/adapters/event-emitter-error-adapter.ts
|
|
1787
|
+
var DEFAULT_EVENT_NAME = "error-log";
|
|
1788
|
+
var EventEmitterErrorAdapter = class {
|
|
1789
|
+
/** @throws {RangeError} if `options.eventName` is empty. */
|
|
1790
|
+
constructor(emitter, options = {}) {
|
|
1791
|
+
this.emitter = emitter;
|
|
1792
|
+
this.eventName = options.eventName ?? DEFAULT_EVENT_NAME;
|
|
1793
|
+
if (this.eventName === "") {
|
|
1794
|
+
throw new RangeError(
|
|
1795
|
+
"EventEmitterErrorAdapter: eventName must not be empty."
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
this.requireListener = options.requireListener ?? false;
|
|
1799
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1800
|
+
}
|
|
1801
|
+
async log(entry) {
|
|
1802
|
+
try {
|
|
1803
|
+
const record = {
|
|
1804
|
+
id: resolveEntryId(entry),
|
|
1805
|
+
...serializeEntry(entry)
|
|
1806
|
+
};
|
|
1807
|
+
const delivered = this.emitter.emit(this.eventName, record);
|
|
1808
|
+
if (this.requireListener && delivered === false) {
|
|
1809
|
+
throw new Error(
|
|
1810
|
+
`no listener is attached to "${this.eventName}", so nothing received the entry`
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
return { ok: true };
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
return reportFailure({
|
|
1816
|
+
adapterName: "EventEmitterErrorAdapter",
|
|
1817
|
+
prefix: this.prefix,
|
|
1818
|
+
action: `to emit "${this.eventName}"`,
|
|
1819
|
+
error
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
// src/adapters/websocket-error-adapter.ts
|
|
1826
|
+
var OPEN = 1;
|
|
1827
|
+
var STATE_NAMES = {
|
|
1828
|
+
0: "still connecting",
|
|
1829
|
+
1: "open",
|
|
1830
|
+
2: "closing",
|
|
1831
|
+
3: "closed"
|
|
1832
|
+
};
|
|
1833
|
+
var WebSocketErrorAdapter = class {
|
|
1834
|
+
constructor(socket, options = {}) {
|
|
1835
|
+
this.socket = socket;
|
|
1836
|
+
this.prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1837
|
+
}
|
|
1838
|
+
async log(entry) {
|
|
1839
|
+
try {
|
|
1840
|
+
const state = this.socket.readyState;
|
|
1841
|
+
if (state !== OPEN) {
|
|
1842
|
+
throw new Error(
|
|
1843
|
+
`the socket is ${STATE_NAMES[state] ?? `in unknown state ${String(state)}`} (readyState ${String(state)}), so the entry was not sent; connect or reconnect it first`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
this.socket.send(
|
|
1847
|
+
JSON.stringify({ id: resolveEntryId(entry), ...serializeEntry(entry) })
|
|
1848
|
+
);
|
|
1849
|
+
return { ok: true };
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
return reportFailure({
|
|
1852
|
+
adapterName: "WebSocketErrorAdapter",
|
|
1853
|
+
prefix: this.prefix,
|
|
1854
|
+
action: "to send over the socket",
|
|
1855
|
+
error
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1642
1860
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1643
1861
|
0 && (module.exports = {
|
|
1644
1862
|
CompositeErrorAdapter,
|
|
1645
1863
|
ConsoleErrorAdapter,
|
|
1864
|
+
EventEmitterErrorAdapter,
|
|
1646
1865
|
FirestoreErrorAdapter,
|
|
1647
1866
|
FlatFileErrorAdapter,
|
|
1648
1867
|
IndexedDbErrorAdapter,
|
|
@@ -1655,6 +1874,7 @@ var SupabaseErrorAdapter = class {
|
|
|
1655
1874
|
S3ErrorAdapter,
|
|
1656
1875
|
SqliteErrorAdapter,
|
|
1657
1876
|
SupabaseErrorAdapter,
|
|
1877
|
+
WebSocketErrorAdapter,
|
|
1658
1878
|
WebStorageErrorAdapter,
|
|
1659
1879
|
WebhookErrorAdapter,
|
|
1660
1880
|
buildCreateErrorLogTableSql,
|