@richardmcquiston01/error-log-adapter 0.4.0 → 0.6.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/dist/index.d.cts CHANGED
@@ -10,6 +10,17 @@ interface ErrorLogEntry<TCode extends string = string> {
10
10
  source?: string;
11
11
  /** ISO 8601 timestamp. Adapters fill this in when it is omitted. */
12
12
  timestamp?: string;
13
+ /**
14
+ * Optional UUID that identifies this entry. Adapters generate a new one for
15
+ * every `log` call when it is omitted, so logging the same entry twice stores
16
+ * it twice. Supply the same `id` (and, for S3, the same `timestamp`) to make
17
+ * a retry safe: sinks that can detect a repeat overwrite or skip the existing
18
+ * record instead of adding another. It must be a UUID, for example from
19
+ * `crypto.randomUUID()`; anything else makes `log` return `ok: false`.
20
+ * Redis, the flat file and NDJSON file adapters, and the console cannot
21
+ * detect a repeat.
22
+ */
23
+ id?: string;
13
24
  }
14
25
  /** Outcome of a `log` call. Callers should check `ok`. */
15
26
  interface ErrorLogResult {
@@ -34,8 +45,16 @@ interface SerializedErrorLogEntry {
34
45
  timestamp: string;
35
46
  }
36
47
  /**
37
- * Converts `Error` instances into plain objects, since `JSON.stringify`
38
- * renders them as `{}`. Other values are returned unchanged.
48
+ * Returns a copy of `cause` that `JSON.stringify` can always handle, so a
49
+ * strange cause can never stop an entry from being stored.
50
+ *
51
+ * - `Error` objects (including ones from another realm) become
52
+ * `{ name, message, stack }`, plus `cause` and, for an `AggregateError`,
53
+ * `errors`, which are converted the same way.
54
+ * - Circular references become `"[Circular]"`, and anything nested deeper than
55
+ * `MAX_CAUSE_DEPTH` levels becomes `"[Truncated]"`.
56
+ * - `BigInt`, `Symbol` and function values, and non-finite numbers, become strings.
57
+ * - A property or `toJSON` that throws is replaced by a short description.
39
58
  */
40
59
  declare function serializeCause(cause: unknown): unknown;
41
60
  /** Returns a JSON-safe copy of the entry, adding a timestamp if missing. */
@@ -56,13 +75,67 @@ interface WebhookErrorOptions {
56
75
  headers?: Record<string, string>;
57
76
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
58
77
  prefix?: string;
78
+ /**
79
+ * Allow a plain `http://` URL. Entries contain error messages and stack
80
+ * traces, so they are only sent over `https://` unless this is `true`.
81
+ * `http://localhost`, `127.0.0.1` and `[::1]` are always allowed.
82
+ * Defaults to `false`.
83
+ */
84
+ allowInsecure?: boolean;
85
+ /**
86
+ * How long to wait for the webhook to answer, in milliseconds. Defaults to
87
+ * 10000. Pass `Infinity` to wait without limit. When it passes, the request is
88
+ * cancelled, so the connection is closed rather than left open; the server may
89
+ * still have received the entry.
90
+ */
91
+ timeoutMs?: number;
92
+ /**
93
+ * How many times to try again after a failure that may go away on its own: a
94
+ * network error, a timeout, HTTP 408, 425, 429 or any 5xx. Other 4xx answers and
95
+ * redirects are never retried. Defaults to 0 (send once). The wait doubles
96
+ * after each attempt, starting at `retryDelayMs`, with jitter, and a
97
+ * `Retry-After` header is honored. `timeoutMs` applies to each attempt, and
98
+ * `log()` resolves only after the last one. Every attempt sends the same body,
99
+ * and with `retries` above 0 the body always carries an `id`, so the receiver
100
+ * can drop a repeat if an earlier attempt did arrive.
101
+ */
102
+ retries?: number;
103
+ /** Wait before the first retry, in milliseconds. Defaults to 500. */
104
+ retryDelayMs?: number;
105
+ /** Longest wait between attempts, in milliseconds. Defaults to 30000. */
106
+ maxRetryDelayMs?: number;
107
+ /**
108
+ * Shared secret for signing each request, so the receiver can verify the
109
+ * entry came from you and was not altered. Adds `X-Signature-Timestamp`
110
+ * (Unix seconds) and `X-Signature-256` (`sha256=` and the hex HMAC-SHA256 of
111
+ * `<timestamp>.<body>`). The receiver should recompute it and reject old
112
+ * timestamps. Needs Web Crypto (Node 20+, browsers, Deno, Bun); without it
113
+ * `log()` fails rather than sending an unsigned entry.
114
+ */
115
+ signingSecret?: string;
59
116
  }
60
117
  declare class WebhookErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
61
118
  private readonly url;
62
119
  private readonly options;
63
120
  private readonly prefix;
121
+ private readonly timeoutMs;
122
+ private readonly retries;
123
+ private readonly retryDelayMs;
124
+ private readonly maxRetryDelayMs;
125
+ /** Scheme and host only. Webhook URLs often carry a secret in the path or query. */
126
+ private readonly target;
127
+ /** @throws {RangeError} if `url` is not a valid `https://` URL (see `allowInsecure`) or a timeout, retry or signing option is invalid. */
64
128
  constructor(url: string, options?: WebhookErrorOptions);
65
129
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
130
+ /** Milliseconds to wait before retry number `retryNumber` (1 for the first). */
131
+ private retryWaitMs;
132
+ private send;
133
+ /** Timestamp and signature headers, or none when no `signingSecret` is set. */
134
+ private signatureHeaders;
135
+ private rejected;
136
+ private redirectMessage;
137
+ /** Replaces the full URL in a message with its origin, in case the runtime put it there. */
138
+ private redact;
66
139
  private fail;
67
140
  }
68
141
 
@@ -73,26 +146,63 @@ interface CollectionLike {
73
146
  interface MongoDbErrorOptions {
74
147
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
75
148
  prefix?: string;
149
+ /**
150
+ * How long `log` waits for the insert before giving up, in milliseconds. Defaults to
151
+ * 10000. Pass `Infinity` to wait without limit. The call cannot be cancelled,
152
+ * so it may still finish after `log` has returned a failure.
153
+ */
154
+ timeoutMs?: number;
76
155
  }
77
156
  declare class MongoDbErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
78
157
  private readonly collection;
79
158
  private readonly prefix;
159
+ private readonly timeoutMs;
160
+ /** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
80
161
  constructor(collection: CollectionLike, options?: MongoDbErrorOptions);
81
162
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
82
163
  }
83
164
 
165
+ interface CompositeErrorOptions {
166
+ /**
167
+ * How long to wait for each adapter before reporting it as failed, in
168
+ * milliseconds. Defaults to 30000, longer than the 10000 default of the built-in
169
+ * adapters so their own, more specific timeout messages come first. This only
170
+ * matters for a custom adapter that can hang: one stuck adapter then cannot
171
+ * hold back the result of the others. Pass `Infinity` to wait without limit.
172
+ */
173
+ timeoutMs?: number;
174
+ }
84
175
  /** Sends every entry to several adapters. Succeeds only if all of them do. */
85
176
  declare class CompositeErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
86
177
  private readonly adapters;
87
- constructor(adapters: ReadonlyArray<ErrorLogAdapter<TCode>>);
178
+ private readonly timeoutMs;
179
+ /** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
180
+ constructor(adapters: ReadonlyArray<ErrorLogAdapter<TCode>>, options?: CompositeErrorOptions);
88
181
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
89
182
  }
90
183
 
91
184
  /** Event handler slot. Typed loosely so real DOM `IDBTransaction` objects fit. */
92
185
  type IdbHandler = ((event: never) => unknown) | null;
186
+ /** The error object of a failed request or transaction (a `DOMException`). */
187
+ interface IdbErrorLike {
188
+ name?: string;
189
+ message?: string;
190
+ }
191
+ /** The subset of an `IDBRequest` this adapter uses. */
192
+ interface IdbRequestLike {
193
+ onerror: IdbHandler;
194
+ readonly error?: IdbErrorLike | null;
195
+ }
93
196
  /** The subset of an `IDBObjectStore` this adapter uses. */
94
197
  interface IdbObjectStoreLike {
198
+ /** Returns an `IDBRequest`. Fakes that return nothing also work. */
95
199
  add(value: unknown): unknown;
200
+ /**
201
+ * Replaces the record with the same key. Used for entries that carry their own
202
+ * `id`, so logging one again does not fail on the existing key. A real
203
+ * `IDBObjectStore` has it; a fake without it falls back to `add`.
204
+ */
205
+ put?(value: unknown): unknown;
96
206
  }
97
207
  /** The subset of an `IDBTransaction` this adapter uses. */
98
208
  interface IdbTransactionLike {
@@ -100,9 +210,7 @@ interface IdbTransactionLike {
100
210
  oncomplete: IdbHandler;
101
211
  onerror: IdbHandler;
102
212
  onabort: IdbHandler;
103
- readonly error?: {
104
- message?: string;
105
- } | null;
213
+ readonly error?: IdbErrorLike | null;
106
214
  }
107
215
  /** The subset of an `IDBDatabase` this adapter uses. */
108
216
  interface IdbDatabaseLike {
@@ -128,6 +236,13 @@ interface IndexedDbErrorOptions {
128
236
  storeName?: string;
129
237
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
130
238
  prefix?: string;
239
+ /**
240
+ * How long `log` waits for the transaction to finish, in milliseconds. Defaults
241
+ * to 10000. Pass `Infinity` to wait without limit. A transaction can stay
242
+ * pending when a version change in another tab blocks it, or when the browser
243
+ * has evicted the origin's storage. The write may still finish afterwards.
244
+ */
245
+ timeoutMs?: number;
131
246
  }
132
247
  /**
133
248
  * Creates the error log object store (UUID `id` key, `timestamp` index).
@@ -138,9 +253,18 @@ declare class IndexedDbErrorAdapter<TCode extends string = string> implements Er
138
253
  private readonly database;
139
254
  private readonly storeName;
140
255
  private readonly prefix;
256
+ private readonly timeoutMs;
257
+ /** @throws {RangeError} if `options.timeoutMs` is not a positive number or Infinity. */
141
258
  constructor(database: IdbDatabaseLike, options?: IndexedDbErrorOptions);
142
259
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
143
- /** Resolves once the transaction commits; rejects if it errors or aborts. */
260
+ /**
261
+ * Resolves once the transaction commits; rejects if the request fails, the
262
+ * transaction aborts, or it does not finish within `timeoutMs`.
263
+ *
264
+ * Error events are handled here (`preventDefault` and `stopPropagation`), so a
265
+ * failed log write does not reach the `onerror` handler an app may have set on
266
+ * its own database object.
267
+ */
144
268
  private write;
145
269
  }
146
270
 
@@ -166,6 +290,12 @@ interface SqliteErrorOptions {
166
290
  createTable?: boolean;
167
291
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
168
292
  prefix?: string;
293
+ /**
294
+ * How long `log` waits for the driver before giving up, in milliseconds. Defaults
295
+ * to 10000. Pass `Infinity` to wait without limit. The call cannot be
296
+ * cancelled, so it may still finish after `log` has returned a failure.
297
+ */
298
+ timeoutMs?: number;
169
299
  }
170
300
  /**
171
301
  * Returns the statements that create the error log table and its index.
@@ -177,13 +307,16 @@ declare class SqliteErrorAdapter<TCode extends string = string> implements Error
177
307
  private readonly tableName;
178
308
  private readonly createTable;
179
309
  private readonly prefix;
310
+ private readonly timeoutMs;
180
311
  private tableReady;
181
- /** @throws {RangeError} if `options.tableName` is not a plain identifier. */
312
+ /** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
182
313
  constructor(client: SqliteClientLike, options?: SqliteErrorOptions);
183
314
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
184
315
  /** Creates the table once. A failed attempt is retried on the next log call. */
185
316
  private ensureTable;
186
317
  private runCreateStatements;
318
+ /** Runs one statement, giving up waiting after `timeoutMs`. */
319
+ private run;
187
320
  }
188
321
 
189
322
  /**
@@ -194,6 +327,14 @@ interface StorageLike {
194
327
  getItem(key: string): string | null;
195
328
  setItem(key: string, value: string): void;
196
329
  }
330
+ /**
331
+ * The part of `navigator.locks` this adapter uses. `localStorage` is shared by
332
+ * every tab of an origin and has no atomic update, so a lock keeps two tabs from
333
+ * overwriting each other's entry.
334
+ */
335
+ interface LockManagerLike {
336
+ request<T>(name: string, callback: () => T | Promise<T>): Promise<T>;
337
+ }
197
338
  /** A stored record: the serialized entry plus its generated UUID. */
198
339
  interface WebStorageErrorRecord extends SerializedErrorLogEntry {
199
340
  id: string;
@@ -205,24 +346,51 @@ interface WebStorageErrorOptions {
205
346
  maxEntries?: number;
206
347
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
207
348
  prefix?: string;
349
+ /**
350
+ * Lock manager that makes tabs sharing a `localStorage` take turns. Defaults to
351
+ * `navigator.locks` when the browser has it. This reduces lost entries but does
352
+ * not remove them: a tab in another process can still read a stale copy
353
+ * (measured in Chromium). Pass `null` to turn locking off.
354
+ */
355
+ locks?: LockManagerLike | null;
208
356
  }
209
357
  /**
210
358
  * Keeps the newest entries in a Web Storage object (`sessionStorage` or
211
359
  * `localStorage`) as a JSON array under one key, capped at `maxEntries`.
212
360
  * When the store is full, the oldest entries are dropped until the new one fits.
361
+ *
362
+ * `sessionStorage` belongs to one tab, so it is safe. `localStorage` is shared by
363
+ * every tab of the origin and has no atomic update: two tabs logging at the same
364
+ * moment can overwrite each other's entry while both get `ok: true`. In Chromium,
365
+ * tabs in separate processes (the usual case) even read a stale copy, so a lock
366
+ * reduces the loss without removing it. Where tabs may log at the same time and
367
+ * every entry matters, use `IndexedDbErrorAdapter`, whose writes are transactional.
213
368
  */
214
369
  declare class WebStorageErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
215
370
  private readonly storage;
216
371
  private readonly key;
217
372
  private readonly maxEntries;
218
373
  private readonly prefix;
374
+ private readonly locks;
219
375
  /** @throws {RangeError} if `options.maxEntries` is not a positive integer. */
220
376
  constructor(storage: StorageLike, options?: WebStorageErrorOptions);
221
377
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
222
- /** Reads, appends, trims, and writes back. Runs without awaiting, so calls never interleave. */
378
+ /**
379
+ * Appends under a cross-tab lock when one is available. If the lock itself
380
+ * cannot be taken (for example a sandboxed frame), the entry is still written
381
+ * without it rather than lost.
382
+ */
383
+ private appendLocked;
384
+ /** Reads, appends, trims, and writes back. Runs without awaiting, so calls in one tab never interleave. */
223
385
  private append;
224
386
  /** Returns the stored records, or none if the value is missing or unreadable. */
225
387
  private readRecords;
388
+ /**
389
+ * Copies an unusable value to `<key>.corrupt` before it is replaced, so data
390
+ * the host app stored under the same key is not silently destroyed. Only the
391
+ * first one is kept; a later unusable value would not overwrite it.
392
+ */
393
+ private setAside;
226
394
  }
227
395
 
228
396
  /**
@@ -281,7 +449,7 @@ declare class FlatFileErrorAdapter<TCode extends string = string> implements Err
281
449
  private readonly prefix;
282
450
  private fs;
283
451
  private directoryReady;
284
- private queue;
452
+ private readonly queue;
285
453
  /** @throws {RangeError} if `maxBytes` or `maxFiles` is not a positive integer. */
286
454
  constructor(filePath: string, options?: FlatFileErrorOptions);
287
455
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
@@ -334,7 +502,7 @@ declare class JsonFileErrorAdapter<TCode extends string = string> implements Err
334
502
  private readonly prefix;
335
503
  private fs;
336
504
  private directoryReady;
337
- private queue;
505
+ private readonly queue;
338
506
  /** @throws {RangeError} if `maxEntries` or `indent` is invalid, or `maxEntries` is used outside array mode. */
339
507
  constructor(filePath: string, options?: JsonFileErrorOptions);
340
508
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
@@ -371,6 +539,12 @@ interface SqlErrorOptions {
371
539
  createTable?: boolean;
372
540
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
373
541
  prefix?: string;
542
+ /**
543
+ * How long `log` waits for each database statement before giving up, in milliseconds. Defaults
544
+ * to 10000. Pass `Infinity` to wait without limit. The call cannot be
545
+ * cancelled, so it may still finish after `log` has returned a failure.
546
+ */
547
+ timeoutMs?: number;
374
548
  }
375
549
  /**
376
550
  * Returns the statements that create the error log table and its index for a
@@ -381,6 +555,12 @@ interface SqlErrorOptions {
381
555
  * cause, and `DATETIME(3)` columns holding UTC times.
382
556
  */
383
557
  declare function buildErrorLogTableDdl(dialect: SqlDialect, tableName?: string): string[];
558
+ /**
559
+ * Returns the statement that widens the `code` and `source` columns of a MySQL
560
+ * table created by `buildErrorLogTableDdl` in 0.4.0 or earlier, where they were
561
+ * `VARCHAR(255)` and a longer value made the insert fail. Run it once.
562
+ */
563
+ declare function buildMySqlWidenColumnsSql(tableName?: string): string[];
384
564
  /** Shared by the Postgres and MySQL adapters: only the SQL text and time format differ. */
385
565
  declare abstract class SqlErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
386
566
  private readonly client;
@@ -389,6 +569,7 @@ declare abstract class SqlErrorAdapter<TCode extends string = string> implements
389
569
  private readonly tableName;
390
570
  private readonly createTable;
391
571
  private readonly prefix;
572
+ private readonly timeoutMs;
392
573
  private tableReady;
393
574
  protected constructor(client: SqlClientLike, dialect: SqlDialect, adapterName: string, options: SqlErrorOptions);
394
575
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
@@ -396,6 +577,8 @@ declare abstract class SqlErrorAdapter<TCode extends string = string> implements
396
577
  /** Creates the table once. A failed attempt is retried on the next log call. */
397
578
  private ensureTable;
398
579
  private runCreateStatements;
580
+ /** Runs one statement, giving up waiting after `timeoutMs`. */
581
+ private query;
399
582
  }
400
583
  /**
401
584
  * Writes entries to Postgres. Pass a `pg` `Pool` or `Client`.
@@ -454,6 +637,12 @@ interface RedisErrorOptions {
454
637
  side?: 'left' | 'right';
455
638
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
456
639
  prefix?: string;
640
+ /**
641
+ * How long `log` waits for each Redis command before giving up, in milliseconds. Defaults
642
+ * to 10000. Pass `Infinity` to wait without limit. The call cannot be
643
+ * cancelled, so it may still finish after `log` has returned a failure.
644
+ */
645
+ timeoutMs?: number;
457
646
  }
458
647
  /**
459
648
  * Writes entries to Redis, either as a capped list of JSON strings or as a
@@ -467,6 +656,7 @@ declare class RedisErrorAdapter<TCode extends string = string> implements ErrorL
467
656
  private readonly maxEntries;
468
657
  private readonly side;
469
658
  private readonly prefix;
659
+ private readonly timeoutMs;
470
660
  /** @throws {RangeError} if `key` is empty, `maxEntries` is not a positive integer, or `mode` / `side` is unknown. */
471
661
  constructor(client: RedisClientLike, options?: RedisErrorOptions);
472
662
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
@@ -476,6 +666,8 @@ declare class RedisErrorAdapter<TCode extends string = string> implements ErrorL
476
666
  * stored by now, so a trim failure is reported but does not fail the call;
477
667
  * the next successful write trims the list again.
478
668
  */
669
+ /** Sends one command, giving up waiting after `timeoutMs`. */
670
+ private send;
479
671
  private trimList;
480
672
  }
481
673
 
@@ -528,6 +720,13 @@ interface S3ErrorOptions {
528
720
  batch?: S3BatchOptions;
529
721
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
530
722
  prefix?: string;
723
+ /**
724
+ * How long an upload may take before `log` (or `flush`) gives up waiting, in
725
+ * milliseconds. Defaults to 10000. Pass `Infinity` to wait without limit. The
726
+ * upload cannot be cancelled, so it may still finish afterwards; in batch mode
727
+ * the entries stay buffered and are uploaded again by the next flush.
728
+ */
729
+ timeoutMs?: number;
531
730
  }
532
731
  /**
533
732
  * Stores entries as objects in an S3-compatible bucket, keyed by date and UUID:
@@ -546,9 +745,10 @@ declare class S3ErrorAdapter<TCode extends string = string> implements ErrorLogA
546
745
  private readonly contentType;
547
746
  private readonly batch;
548
747
  private readonly logPrefix;
748
+ private readonly timeoutMs;
549
749
  private buffer;
550
750
  private timer;
551
- private flushQueue;
751
+ private readonly flushQueue;
552
752
  /** @throws {RangeError} if `bucket` is empty or the batch options are invalid. */
553
753
  constructor(client: S3ClientLike, bucket: string, options?: S3ErrorOptions);
554
754
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
@@ -604,11 +804,19 @@ interface FirestoreCollectionLike {
604
804
  * ```ts
605
805
  * const refLike: RealtimeDatabaseRefLike = {
606
806
  * push: (value) => push(ref(db, 'error_log'), value),
807
+ * child: (key) => ({ set: (value) => set(ref(db, `error_log/${key}`), value) }),
607
808
  * };
608
809
  * ```
810
+ *
811
+ * `child(key).set(value)` is only needed for entries that carry their own `id`
812
+ * (see `ErrorLogEntry.id`): they are written under that key, so logging the same
813
+ * entry again overwrites the record instead of adding a second one.
609
814
  */
610
815
  interface RealtimeDatabaseRefLike {
611
816
  push(value: FirebaseErrorRecord): PromiseLike<unknown>;
817
+ child?(key: string): {
818
+ set(value: FirebaseErrorRecord): PromiseLike<unknown>;
819
+ };
612
820
  }
613
821
  interface FirebaseErrorOptions {
614
822
  /**
@@ -618,7 +826,9 @@ interface FirebaseErrorOptions {
618
826
  * The Firebase SDKs keep writes queued while the connection is down, and
619
827
  * their promises stay pending until it returns, so without a limit `log`
620
828
  * could hang for as long as an outage lasts. A write that timed out is still
621
- * queued by the SDK and may be saved later.
829
+ * queued by the SDK and may be saved later. Retry a timeout only with the same
830
+ * `ErrorLogEntry.id`, so the retry overwrites the first write instead of
831
+ * adding a second one.
622
832
  */
623
833
  timeoutMs?: number;
624
834
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
@@ -626,7 +836,11 @@ interface FirebaseErrorOptions {
626
836
  }
627
837
  /**
628
838
  * Writes each entry as a document in a Firestore collection. The document id is
629
- * the entry's UUID, so retrying a write never creates a duplicate.
839
+ * the entry's UUID, so the SDK finishing one queued write twice cannot create two
840
+ * documents. Calling `log` again without an `id` on the entry is a new write
841
+ * with a new UUID: if a call reported a timeout, the first write may still be
842
+ * saved later, and logging the same entry again can store it twice. Give the
843
+ * entry an `id` and the retry overwrites the same document.
630
844
  */
631
845
  declare class FirestoreErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
632
846
  private readonly collection;
@@ -684,26 +898,46 @@ interface SupabaseClientLike {
684
898
  };
685
899
  }
686
900
  interface SupabaseErrorOptions {
687
- /** Table to write to. Defaults to `error_log`. */
901
+ /** Table to write to. Defaults to `error_log`. Letters, digits and `_` only. */
688
902
  tableName?: string;
689
903
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
690
904
  prefix?: string;
905
+ /**
906
+ * How long `log` waits for Supabase to answer before giving up, in milliseconds. Defaults
907
+ * to 10000. Pass `Infinity` to wait without limit. The call cannot be
908
+ * cancelled, so it may still finish after `log` has returned a failure.
909
+ */
910
+ timeoutMs?: number;
691
911
  }
692
912
  interface SupabaseSetupOptions {
693
913
  /**
694
- * Postgres roles allowed to insert, for example `['anon', 'authenticated']`
695
- * when the browser logs with the public key. Leave empty when only a server
696
- * using the service role key logs: that role bypasses row level security.
914
+ * Postgres roles allowed to insert through the API, for example
915
+ * `['authenticated']`. Leave empty when only a server using the service role
916
+ * key logs: that role bypasses row level security, so it needs no policy, and
917
+ * its key must never be shipped to a browser.
918
+ *
919
+ * The generated policy only checks the size of each field. It does not tie a
920
+ * row to a user, so any holder of an allowed role's key can still add entries
921
+ * of their own making: forged entries can hide real incidents, and a flood of
922
+ * them uses up your database quota. Prefer `authenticated` over `anon`.
697
923
  */
698
924
  insertRoles?: string[];
925
+ /**
926
+ * Required to allow `anon` (or `public`) to insert. The `anon` key is part of
927
+ * your frontend, so this lets anyone who loads the page write to the log.
928
+ * Defaults to `false`.
929
+ */
930
+ allowPublicInsert?: boolean;
699
931
  }
700
932
  /**
701
933
  * Returns the SQL to run once in the Supabase SQL editor: the error log table,
702
934
  * with row level security turned on (as Supabase recommends for every table in
703
935
  * an exposed schema) and, optionally, an insert-only policy for the given roles.
704
- * Nobody but the service role can read the log through the API.
936
+ * The policy limits the size of each field. Nobody but the service role can read
937
+ * the log through the API.
705
938
  *
706
- * @throws {RangeError} if the table name or a role is not a plain identifier.
939
+ * @throws {RangeError} if the table name or a role is not a plain identifier, or
940
+ * `anon` / `public` is listed without `allowPublicInsert: true`.
707
941
  */
708
942
  declare function buildSupabaseSetupSql(tableName?: string, options?: SupabaseSetupOptions): string[];
709
943
  /**
@@ -721,10 +955,71 @@ declare class SupabaseErrorAdapter<TCode extends string = string> implements Err
721
955
  private readonly client;
722
956
  private readonly tableName;
723
957
  private readonly prefix;
724
- /** @throws {RangeError} if `options.tableName` is empty. */
958
+ private readonly timeoutMs;
959
+ /** @throws {RangeError} if `options.tableName` is not a plain identifier or `options.timeoutMs` is invalid. */
725
960
  constructor(client: SupabaseClientLike, options?: SupabaseErrorOptions);
726
961
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
727
962
  private fail;
728
963
  }
729
964
 
730
- export { type CollectionLike, CompositeErrorAdapter, ConsoleErrorAdapter, type ConsoleErrorOptions, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
965
+ /** What listeners receive: the serialized entry plus its UUID. */
966
+ interface EventEmitterErrorRecord extends SerializedErrorLogEntry {
967
+ id: string;
968
+ }
969
+ /**
970
+ * The only thing this adapter needs from an emitter: `emit(eventName, payload)`.
971
+ * Node's `EventEmitter` and mitt-style emitters fit as they are. A browser
972
+ * `EventTarget` needs a one-line wrapper:
973
+ *
974
+ * ```ts
975
+ * const target = new EventTarget();
976
+ * const emitter: EmitterLike = {
977
+ * emit: (name, payload) =>
978
+ * target.dispatchEvent(new CustomEvent(name, { detail: payload })),
979
+ * };
980
+ * ```
981
+ */
982
+ interface EmitterLike {
983
+ emit(eventName: string, payload: EventEmitterErrorRecord): unknown;
984
+ }
985
+ interface EventEmitterErrorOptions {
986
+ /**
987
+ * Name of the event to emit. Defaults to `error-log`. Avoid `error` on a Node
988
+ * `EventEmitter`: it throws when no listener is attached, and `log` would then
989
+ * return `ok: false`.
990
+ */
991
+ eventName?: string;
992
+ /**
993
+ * Treat an emit that reached nobody as a failure. Node's `EventEmitter.emit`
994
+ * returns `false` when there are no listeners; with this set to `true`, `log`
995
+ * then returns `ok: false` instead of letting the entry vanish. Emitters whose
996
+ * `emit` returns anything else (mitt, an `EventTarget` wrapper) cannot tell,
997
+ * so they always count as delivered. Defaults to `false`.
998
+ */
999
+ requireListener?: boolean;
1000
+ /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
1001
+ prefix?: string;
1002
+ }
1003
+ /**
1004
+ * Emits each entry as an event on an emitter you own, for in-process hooks such
1005
+ * as a toast, a metrics counter, or a test assertion. Listeners run
1006
+ * synchronously inside `emit`, so a listener that throws makes `log` return
1007
+ * `ok: false` with the listener's message; the application is not affected.
1008
+ *
1009
+ * ```ts
1010
+ * const bus = new EventEmitter();
1011
+ * bus.on('error-log', (record) => showToast(record.message));
1012
+ * const adapter = new EventEmitterErrorAdapter(bus);
1013
+ * ```
1014
+ */
1015
+ declare class EventEmitterErrorAdapter<TCode extends string = string> implements ErrorLogAdapter<TCode> {
1016
+ private readonly emitter;
1017
+ private readonly eventName;
1018
+ private readonly requireListener;
1019
+ private readonly prefix;
1020
+ /** @throws {RangeError} if `options.eventName` is empty. */
1021
+ constructor(emitter: EmitterLike, options?: EventEmitterErrorOptions);
1022
+ log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
1023
+ }
1024
+
1025
+ export { type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type EmitterLike, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, EventEmitterErrorAdapter, type EventEmitterErrorOptions, type EventEmitterErrorRecord, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };