@bazariodev/docket 0.0.0-stage → 1.0.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 (96) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +985 -2
  4. package/dist/browser/constants.d.cts +14 -0
  5. package/dist/browser/constants.d.ts +14 -0
  6. package/dist/browser/index.cjs +192 -0
  7. package/dist/browser/index.cjs.map +1 -0
  8. package/dist/browser/index.d.cts +29 -0
  9. package/dist/browser/index.d.ts +29 -0
  10. package/dist/browser/index.js +187 -0
  11. package/dist/browser/index.js.map +1 -0
  12. package/dist/chunk-7ld72rnl.js +376 -0
  13. package/dist/chunk-7ld72rnl.js.map +1 -0
  14. package/dist/chunk-BLf-eCDO.cjs +53 -0
  15. package/dist/chunk-BLf-eCDO.cjs.map +1 -0
  16. package/dist/chunk-CEhpwtwJ.cjs +379 -0
  17. package/dist/chunk-CEhpwtwJ.cjs.map +1 -0
  18. package/dist/chunk-CSS3f4_M.js +46 -0
  19. package/dist/chunk-CSS3f4_M.js.map +1 -0
  20. package/dist/chunk-CXZYNALm.cjs +124 -0
  21. package/dist/chunk-CXZYNALm.cjs.map +1 -0
  22. package/dist/chunk-DYcNnkOV.js +50 -0
  23. package/dist/chunk-DYcNnkOV.js.map +1 -0
  24. package/dist/chunk-DkvWLziD.js +437 -0
  25. package/dist/chunk-DkvWLziD.js.map +1 -0
  26. package/dist/chunk-DqK_3eFr.cjs +53 -0
  27. package/dist/chunk-DqK_3eFr.cjs.map +1 -0
  28. package/dist/chunk-LRCHwVuE.js +110 -0
  29. package/dist/chunk-LRCHwVuE.js.map +1 -0
  30. package/dist/chunk-ieB8yTEn.cjs +439 -0
  31. package/dist/chunk-ieB8yTEn.cjs.map +1 -0
  32. package/dist/client/index.cjs +167 -0
  33. package/dist/client/index.cjs.map +1 -0
  34. package/dist/client/index.d.cts +40 -0
  35. package/dist/client/index.d.ts +40 -0
  36. package/dist/client/index.js +165 -0
  37. package/dist/client/index.js.map +1 -0
  38. package/dist/constants.d.cts +4 -0
  39. package/dist/constants.d.ts +4 -0
  40. package/dist/core/constants.d.cts +43 -0
  41. package/dist/core/constants.d.ts +43 -0
  42. package/dist/devtools/index.cjs +37 -0
  43. package/dist/devtools/index.cjs.map +1 -0
  44. package/dist/devtools/index.d.cts +11 -0
  45. package/dist/devtools/index.d.ts +11 -0
  46. package/dist/devtools/index.js +35 -0
  47. package/dist/devtools/index.js.map +1 -0
  48. package/dist/export/index.cjs +139 -0
  49. package/dist/export/index.cjs.map +1 -0
  50. package/dist/export/index.d.cts +49 -0
  51. package/dist/export/index.d.ts +49 -0
  52. package/dist/export/index.js +137 -0
  53. package/dist/export/index.js.map +1 -0
  54. package/dist/history/index.cjs +30 -0
  55. package/dist/history/index.cjs.map +1 -0
  56. package/dist/history/index.d.cts +11 -0
  57. package/dist/history/index.d.ts +11 -0
  58. package/dist/history/index.js +28 -0
  59. package/dist/history/index.js.map +1 -0
  60. package/dist/index.cjs +14 -0
  61. package/dist/index.cjs.map +1 -0
  62. package/dist/index.d.cts +159 -0
  63. package/dist/index.d.ts +159 -0
  64. package/dist/index.js +6 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/indexeddb/index.cjs +26 -0
  67. package/dist/indexeddb/index.cjs.map +1 -0
  68. package/dist/indexeddb/index.d.cts +61 -0
  69. package/dist/indexeddb/index.d.ts +61 -0
  70. package/dist/indexeddb/index.js +23 -0
  71. package/dist/indexeddb/index.js.map +1 -0
  72. package/dist/redaction/index.cjs +154 -0
  73. package/dist/redaction/index.cjs.map +1 -0
  74. package/dist/redaction/index.d.cts +70 -0
  75. package/dist/redaction/index.d.ts +70 -0
  76. package/dist/redaction/index.js +152 -0
  77. package/dist/redaction/index.js.map +1 -0
  78. package/dist/sip/index.cjs +336 -0
  79. package/dist/sip/index.cjs.map +1 -0
  80. package/dist/sip/index.d.cts +34 -0
  81. package/dist/sip/index.d.ts +34 -0
  82. package/dist/sip/index.js +333 -0
  83. package/dist/sip/index.js.map +1 -0
  84. package/dist/storage/constants.d.cts +11 -0
  85. package/dist/storage/constants.d.ts +11 -0
  86. package/dist/storage/contracts.d.cts +31 -0
  87. package/dist/storage/contracts.d.ts +31 -0
  88. package/dist/upload/constants.d.cts +17 -0
  89. package/dist/upload/constants.d.ts +17 -0
  90. package/dist/upload/index.cjs +246 -0
  91. package/dist/upload/index.cjs.map +1 -0
  92. package/dist/upload/index.d.cts +51 -0
  93. package/dist/upload/index.d.ts +51 -0
  94. package/dist/upload/index.js +244 -0
  95. package/dist/upload/index.js.map +1 -0
  96. package/package.json +150 -3
package/README.md CHANGED
@@ -1,3 +1,986 @@
1
- # Temporary Holding Version
1
+ # @bazariodev/docket
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Record structured diagnostic data. Remove private values before delivery, keep bounded history,
4
+ and build ZIP files for download or application upload.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ pnpm add @bazariodev/docket
10
+ ```
11
+
12
+ Docket uses [Coordination](https://www.npmjs.com/package/@bazariodev/coordination) as a runtime
13
+ dependency. The `/history` entry creates its private `Queue` directly. The root entry has no
14
+ external runtime imports. Public declarations use local types and structural provider ports.
15
+ Memory history, upload, conversion, persistence, SIP, and ZIP support are optional entries.
16
+ Import only the entries that the application uses.
17
+
18
+ The usage example selects [Winnow](https://www.npmjs.com/package/@bazariodev/winnow) for bounded
19
+ conversion:
20
+
21
+ ```sh
22
+ pnpm add @bazariodev/winnow
23
+ ```
24
+
25
+ Other providers can implement the same structural ports. A selected provider can install its own
26
+ dependencies. Docket does not change that provider's package graph. Optional examples below also
27
+ select [Baler](https://www.npmjs.com/package/@bazariodev/baler) for ZIP files and
28
+ [Hopper](https://www.npmjs.com/package/@bazariodev/hopper) for standalone persistence.
29
+ Install these packages only for the corresponding application composition.
30
+
31
+ Node.js support is `^22.12.0 || ^24.0.0 || >=26.0.0`. Browser persistence needs the selected storage
32
+ provider's platform APIs. The Hopper IndexedDB composition uses IndexedDB and Web Locks.
33
+ Memory history and memory outboxes use no device storage or application lease.
34
+
35
+ ## Usage
36
+
37
+ ```js
38
+ import { createDocket } from '@bazariodev/docket/client';
39
+ import { createRedactor } from '@bazariodev/docket/redaction';
40
+ import * as conversion from '@bazariodev/winnow';
41
+
42
+ const redactor = createRedactor(
43
+ { fields: { password: 'drop', accessToken: 'drop' } },
44
+ conversion,
45
+ );
46
+ const docket = createDocket({
47
+ areas: [{ id: 'application' }, { id: 'network', level: 'warn' }],
48
+ redactor,
49
+ });
50
+ docket.getLogger('application', 'Connection').info('Connection ready');
51
+ const records = await docket.history?.query();
52
+ ```
53
+
54
+ This call keeps the last 1,000 records in memory. Console output and upload are off.
55
+ It uses no device storage.
56
+ Supply your application's area list and privacy policy. Default text rules do not identify every
57
+ private field in an application.
58
+
59
+ | Task | Example or guidance |
60
+ | --- | --- |
61
+ | Keep and query local history | [Memory history](#memory-history). |
62
+ | Add console output or a custom sink | [Console output and custom sinks](#console-output-and-custom-sinks). |
63
+ | Keep history and upload independently | [Storage and delivery options](#storage-and-delivery-options). |
64
+ | Add correlation context, Errors, or lazy data | [Context, Errors, and lazy data](#context-errors-and-lazy-data). |
65
+ | Produce JSON or NDJSON | [JSON and NDJSON](#json-and-ndjson). |
66
+ | Select capacity and bound input cost | [Capacity and input cost](#capacity-and-input-cost). |
67
+ | Flush, stop, erase, and remove browser listeners | [Shutdown and account changes](#shutdown-and-account-changes). |
68
+
69
+ ## Storage and delivery options
70
+
71
+ History and upload are independent. No strategy name or storage registry is required.
72
+
73
+ | Options | Result |
74
+ | --- | --- |
75
+ | Neither option | Cyclic memory history; no upload. |
76
+ | `history: { maxRecords: 2000 }` | Cyclic memory history with another capacity. |
77
+ | `history: historyClient` | Use a supplied memory, IndexedDB, or other compatible client. |
78
+ | `history: false`, `upload: { ... }` | Upload only. |
79
+ | `history: { ... }`, `upload: { ... }` | History and upload. |
80
+ | `upload: false` or omitted upload | No upload. |
81
+
82
+ Supply Hopper clients directly, or supply other clients with the same public methods.
83
+ Open persistent clients only after authentication. The application supplies an opaque
84
+ `authenticatedSession.storageKey` and an `uploadRecords` callback bound to that same account.
85
+ Use a different scope for each tenant and account:
86
+
87
+ ```js
88
+ import { createIndexedDbStore } from '@bazariodev/hopper/indexeddb';
89
+ import { createIndexedDbOutbox } from '@bazariodev/hopper/outbox/indexeddb';
90
+
91
+ const scope = authenticatedSession.storageKey;
92
+ const names = (kind) => ({
93
+ databaseName: JSON.stringify(['diagnostics', scope, kind]),
94
+ lockName: JSON.stringify(['diagnostics-owner', scope, kind]),
95
+ });
96
+ const storageOptions = { history: names('history'), outbox: names('outbox') };
97
+ const historyClient = createIndexedDbStore(storageOptions.history);
98
+ const outbox = await createIndexedDbOutbox({
99
+ ...storageOptions.outbox,
100
+ maxRecords: 1000,
101
+ });
102
+ const persistent = createDocket({
103
+ areas: [{ id: 'application' }],
104
+ redactor,
105
+ history: historyClient,
106
+ upload: {
107
+ outbox,
108
+ send: uploadRecords,
109
+ },
110
+ });
111
+ ```
112
+
113
+ The client selects storage. Docket imports no Hopper code. For memory upload, supply
114
+ `createMemoryOutbox({ maxRecords })` from `@bazariodev/hopper/outbox`. The outbox is required;
115
+ its factory owns capacity and storage configuration. Docket does not copy those options.
116
+
117
+ A history client needs `write`, `query`, and `clear`; `flush`, `stop`, and `getStatus` are optional.
118
+ An outbox needs the public upload port plus `clear` and `stop`. Methods keep their receivers.
119
+ The returned `history` and `outbox` are the supplied clients. Do not use one client for both
120
+ roles or share owned clients between compositions. A successful `createDocket` call owns their
121
+ shutdown. Failed construction leaves supplied clients open and cancels its new upload timer.
122
+
123
+ History keeps records for download. The outbox keeps records until server acceptance. They are
124
+ separate stores: upload success must not erase local history. Clearing history must not remove
125
+ pending upload records. Current Hopper history and outbox schemas need separate databases.
126
+ Database and lock names come from initial client options. Use the same lock name for all writers
127
+ of one database. Hopper history keeps its timestamp-order and identity rules; the default cyclic
128
+ history keeps FIFO order and duplicate admissions.
129
+
130
+ Upload defaults to 100 records per batch and a 5,000 ms interval. Supply
131
+ `send(records, signal, token)`. The consumer owns the endpoint, credentials, encoding,
132
+ request timeout, retry, response checks, and transport error reporting. Docket contains no HTTP client.
133
+
134
+ Resolve the callback only after the backend accepts the complete batch. After handling a failure,
135
+ throw or reject so Docket retains the batch and token. Catching an error and resolving reports
136
+ success and permits acknowledgement. For example, the application can supply its API client
137
+ and error reporter:
138
+
139
+ ```js
140
+ async function uploadRecords(records, signal, token) {
141
+ try {
142
+ await api.uploadLogsWithRetry({ records, token }, { signal });
143
+ } catch (error) {
144
+ reportUploadError(error);
145
+ throw error;
146
+ }
147
+ }
148
+ ```
149
+
150
+ The application supplies `api.uploadLogsWithRetry` and `reportUploadError`. It must make API resolution mean complete
151
+ acceptance. The backend must deduplicate the batch token in the application scope. A lost response
152
+ or acknowledgement can cause a retry. Docket owns batching, one active callback, scheduling,
153
+ retained batch protection, and acknowledgement. The callback must observe its abort signal.
154
+ Supply a finite request timeout in the callback when required.
155
+
156
+ `onError` is optional. Use it to observe storage or processor failures. If the callback reports
157
+ transport errors itself, avoid reporting them again through this observer. Manual flush rejects
158
+ on failure. A rejected callback retains the batch and pauses automatic attempts. New writes do not
159
+ resume them. Call `flush()` explicitly to resume. Transport backoff stays inside the consumer callback.
160
+
161
+ ### Resume a paused upload
162
+
163
+ For background upload, supply `upload.onError` or check `getStatus().paused` in the application's
164
+ status updates. Otherwise a rejected callback can leave upload paused without an application
165
+ notification. Writes continue during a pause. At capacity, the outbox evicts old unprotected
166
+ records or drops incoming records when the protected batch fills capacity. Check `droppedRecords`.
167
+ Resuming does not restore records lost to capacity.
168
+
169
+ Any explicit processor or hub flush resumes upload. This includes `docket.flush()` and a call
170
+ made by `flushOnPageHide(docket)`. Docket installs no page-exit listener itself. To flush only
171
+ local history on page exit, pass `{ flush: () => historyClient.flush() }` to that helper instead.
172
+ Keep offline, call-state, and transport retry gates inside the consumer's send callback. A pending
173
+ callback must observe its abort signal; a rejected callback pauses automatic attempts.
174
+
175
+ The consumer can retry after an explicit user action or a connectivity event. This example uses
176
+ the application's `redactor`, `outbox`, `uploadRecords`, `refreshUploadStatus`, and retry button.
177
+ The status helper updates the interface; the send callback reports transport errors:
178
+
179
+ ```js
180
+ const docket = createDocket({
181
+ areas: [{ id: 'application' }],
182
+ redactor,
183
+ upload: {
184
+ outbox,
185
+ send: uploadRecords,
186
+ onError: refreshUploadStatus,
187
+ },
188
+ });
189
+ const listenerLifetime = new AbortController();
190
+
191
+ async function resumeUpload() {
192
+ const upload = docket.upload;
193
+ const status = upload?.getStatus();
194
+ if (!status?.paused || status.stopped) return;
195
+ try {
196
+ await upload.flush();
197
+ } catch {
198
+ // The upload observer updates the interface. Keep the batch paused.
199
+ }
200
+ }
201
+
202
+ const resume = () => void resumeUpload();
203
+ const listenerOptions = { signal: listenerLifetime.signal };
204
+ retryButton.addEventListener('click', resume, listenerOptions);
205
+ window.addEventListener('online', resume, listenerOptions);
206
+
207
+ async function stopDocket() {
208
+ listenerLifetime.abort();
209
+ await docket.stop();
210
+ }
211
+ ```
212
+
213
+ The `online` event is a retry trigger, not proof that the backend is available. Another rejection
214
+ keeps the batch paused. The consumer can call the same resume function from its own backoff
215
+ scheduler. Run it after the error observer returns; a synchronous flush inside that observer
216
+ joins the failed operation. Keep resume work bound to the same account. Remove listeners and
217
+ cancel consumer retry timers before account replacement or terminal shutdown.
218
+
219
+ Call `await docket.flush()` for delivery. `docket.stop()` closes admission and cancels upload
220
+ before it flushes and stops history and other sinks. It then stops the outbox, including after
221
+ another cleanup step fails. Stop makes no final request and preserves persistent pending records.
222
+ It is terminal and idempotent. Default memory history remains readable after stop.
223
+
224
+ For logout, first close application admission, for example through `isActive`. Then stop the
225
+ uploader, clear the outbox, stop the composition, and clear history:
226
+
227
+ This flow intentionally discards pending records. If policy requires delivery first, await the
228
+ old account's upload flush before stop, with its original credentials. On failure, retain the
229
+ batch or report an explicit discard. Stop itself makes no final request.
230
+
231
+ ```js
232
+ const failures = [];
233
+ for (const step of [
234
+ () => persistent.resetLevel(),
235
+ () => persistent.upload.stop(),
236
+ () => persistent.outbox.clear(),
237
+ () => persistent.stop(),
238
+ () => persistent.history.clear(),
239
+ ]) {
240
+ try {
241
+ await step();
242
+ } catch (error) {
243
+ failures.push(error);
244
+ }
245
+ }
246
+ if (failures.length) throw new AggregateError(failures, 'Cleanup failed');
247
+ ```
248
+
249
+ Hopper outbox clear requires an active adapter. Do not clear it after outbox stop.
250
+ The application owns account changes and credentials. No account controller or automatic disk
251
+ cleanup is added.
252
+
253
+ After a crash or forced sign-out, the next account must use its own resource names. Do not open
254
+ an old account's outbox with a new account's sender. Keep the old names if cleanup needs retry.
255
+ When no old client holds the locks, use Hopper `clearIndexedDbStore(storageOptions.history)` and
256
+ `clearIndexedDbOutbox(storageOptions.outbox)`. Both clear existing storage without creating a
257
+ database. Use the outbox helper for outboxes; history cleanup removes different metadata.
258
+
259
+ ## Low-level composition
260
+
261
+ Use `createLogging` when the application already owns its sinks and shutdown order:
262
+
263
+ ```js
264
+ import { createLogging } from '@bazariodev/docket';
265
+ import { createMemoryHistory } from '@bazariodev/docket/history';
266
+
267
+ const history = createMemoryHistory({ maxRecords: 1000 });
268
+ const logging = createLogging({
269
+ areas: [{ id: 'application' }, { id: 'network', level: 'warn' }],
270
+ redactor,
271
+ sinks: [history],
272
+ });
273
+ ```
274
+
275
+ The later examples reuse `conversion`, `redactor`, `history`, and `logging` from these examples.
276
+ Each memory history owns a separate Coordination queue. The application supplies no queue factory.
277
+
278
+ ### Console output and custom sinks
279
+
280
+ Console output requires an explicit sink. This example adds it to the hub above:
281
+
282
+ ```js
283
+ import { createConsoleSink } from '@bazariodev/docket';
284
+
285
+ const detachConsole = logging.addSink(createConsoleSink({ level: 'info' }));
286
+ ```
287
+
288
+ A custom sink receives redacted, frozen records. Its `write(record)` must complete admission
289
+ synchronously. The hub ignores its return value. It does not track returned promises, wait for
290
+ them during flush, or contain their asynchronous rejections. For example:
291
+
292
+ ```js
293
+ const detachOutput = logging.addSink({
294
+ write(record) {
295
+ output(record);
296
+ },
297
+ });
298
+ ```
299
+
300
+ The application supplies the synchronous `output` function. Use
301
+ [the upload sink](#periodic-upload) and its `send` callback for backend delivery.
302
+ A custom asynchronous destination must own its bounded buffer and failure handling.
303
+ Its optional `flush()` must wait for work admitted before that call. Its optional `stop()`
304
+ must release its resources. The hub awaits these lifecycle methods.
305
+
306
+ Call `detachOutput()` or `detachConsole()` to stop future delivery to that registration.
307
+ Detach does not flush or stop the sink. The application must complete its cleanup separately.
308
+
309
+ ### Page-lifetime loggers and account sinks
310
+
311
+ Keep one root hub when modules cache their loggers for the whole page. Stop is terminal;
312
+ cached loggers from a stopped hub ignore later writes. Creating a new account client cannot
313
+ redirect those loggers. Use the same page hub and replace its account sinks instead:
314
+
315
+ ```js
316
+ const logging = createLogging({
317
+ areas: [{ id: 'application' }],
318
+ redactor,
319
+ isActive: () => accountActive,
320
+ });
321
+ const logger = logging.getLogger('application', 'Connection');
322
+ const detachHistory = logging.addSink(accountHistory);
323
+ const detachUpload = logging.addSink(accountUpload);
324
+ ```
325
+
326
+ The application supplies the gate and both account sinks. Close admission before an account
327
+ change. Reset levels while the hub is active, detach old sinks, then stop and erase those old
328
+ clients. Attach new sinks before opening admission. Do not call hub stop at account boundaries.
329
+ The hub keeps one `runId` and increasing sequence numbers across accounts. `copyRecords` can
330
+ copy a pre-account source when application policy permits it; attaching a sink copies no backlog.
331
+
332
+ ### Level overrides
333
+
334
+ `levelStorage` is optional. Without it, overrides live only in the hub. Supply a distinct key
335
+ for each tenant, account, and hub. For example, use
336
+ `JSON.stringify(['diagnostic-levels', accountStorageKey, hubId])` with the application's storage.
337
+ Do not share one key between hubs or reuse an old account's key for another account.
338
+ Saving replaces that key with this hub's active overrides; it does not merge other areas.
339
+
340
+ Call `resetLevel()` before stop for logout. Reset after stop does nothing. For a page-lifetime
341
+ hub, reset before account replacement, select the new account's storage, then call `reloadLevels()`.
342
+ Use a stable storage port that routes operations to the current account key. Replacing the
343
+ constructor option does not replace the hub's captured port.
344
+ The application owns storage permission, cross-tab notifications, and removal of old keys.
345
+ Expiry uses wall-clock timestamps. A backward clock adjustment can extend elapsed duration;
346
+ `maxOverrideMs` does not promise a monotonic elapsed-time deadline.
347
+
348
+ ### Context, Errors, and lazy data
349
+
350
+ Keep scope names stable. Put variable correlation IDs in child context:
351
+
352
+ ```js
353
+ const log = logging.getLogger('application', 'Request');
354
+ const requestLog = log.child({ context: { requestId: 'request-123' } });
355
+ requestLog.error('Request failed', new Error('Timed out'));
356
+ requestLog.debug('Request counters', () => readRequestCounters());
357
+ ```
358
+
359
+ The application supplies the synchronous `readRequestCounters` function. Docket calls it only
360
+ after the initial admission checks. Later state changes or record validation can still discard
361
+ the record. Select a few diagnostic fields; do not return the whole application store.
362
+ The redactor still applies the application's privacy policy to context, Errors, and lazy data.
363
+
364
+ ## Consumer redaction policy
365
+
366
+ The application selects which values to keep, remove, mask, or hash. Redaction is the complete
367
+ process; masking and hashing are two possible actions. The package does not require hashing for
368
+ core logging, SIP, storage, or export.
369
+
370
+ ```js
371
+ import { createRedactor } from '@bazariodev/docket/redaction';
372
+
373
+ const redactor = createRedactor({
374
+ fields: { password: 'drop', email: 'mask', state: 'keep' },
375
+ unknownFields: 'drop',
376
+ }, conversion);
377
+ ```
378
+
379
+ Field names match without case at every data depth, including context and Error properties.
380
+
381
+ | Action | Result |
382
+ | --- | --- |
383
+ | `keep` | Keep the field and apply nested and text rules. This does not bypass text redaction. |
384
+ | `drop` | Remove the field. |
385
+ | `mask` | Hide part of the value. Short strings are hidden completely. Longer digit strings keep the last four digits; other strings keep the first and last character. |
386
+ | `hash` | Replace the value with the result of the application's hash function. |
387
+
388
+ Use `text` rules to replace selected text with a fixed value such as `[redacted]`, or to apply
389
+ another application rule. Custom `policy.text` rules receive the Docket record API. An explicit
390
+ `text` list replaces the default list. `text: []` disables text rules. Provider defaults belong in
391
+ `conversion.TEXT_RULES`; their callbacks receive the value engine API. To select a provider subset,
392
+ supply a dictionary such as `{ email: TEXT_RULES.email }` as `TEXT_RULES` and omit `policy.text`.
393
+ Unknown fields default to `keep`; use `unknownFields: 'drop'` when only named fields are permitted.
394
+
395
+ Storm summaries also use this field policy. To retain the `logging.suppressed` counts with
396
+ `unknownFields: 'drop'`, allow `scope` and `dropped` with the `keep` action. Otherwise the policy
397
+ can remove those summary fields.
398
+
399
+ Field mask/hash actions convert numbers and bigint to text. Other non-string values use a fixed
400
+ redaction marker without a hash call. Error schema field rules use the same actions. Captured runtime
401
+ accessors supply Error stacks and DOMException fields. Runtime stack formatting can call an
402
+ application `Error.prepareStackTrace` function.
403
+
404
+ Hashing is optional. The application supplies the function and owns its algorithm and key.
405
+ For example, the application can call this factory with its synchronous keyed hasher:
406
+
407
+ ```js
408
+ import { createRedactor } from '@bazariodev/docket/redaction';
409
+
410
+ export function createApplicationRedactor(hashPrivateValue, conversion) {
411
+ return createRedactor({
412
+ hash: hashPrivateValue,
413
+ fields: { customerId: 'hash', password: 'drop' },
414
+ unknownFields: 'drop',
415
+ }, conversion);
416
+ }
417
+ ```
418
+
419
+ Text-rule methods use the compiled rule as their receiver and receive the Docket redactor API.
420
+ Docket captures the hash function at construction and calls it with the original policy as its receiver.
421
+ Prototype hash methods are supported. Later changes to `policy.hash` do not replace the captured
422
+ function; the captured method can still read current policy state.
423
+
424
+ The hash function must return a string synchronously. A Promise is not supported. A hash rule
425
+ without a function rejects policy creation. The package checks the output type and limits; it
426
+ does not verify the algorithm's security. Keep secret keys out of records and exports. Keyed
427
+ tokens can connect equal values under the same key; they do not encrypt the logs.
428
+
429
+ An application can also supply its own `Redactor`. The hub still validates, copies, and freezes
430
+ its output within the declared limits. These checks do not prove that a privacy policy removes
431
+ all private data. Select the policy before recording. Export cannot restore removed values.
432
+
433
+ ## SIP display names
434
+
435
+ ```js
436
+ import { createRedactor } from '@bazariodev/docket/redaction';
437
+ import { createSipTools } from '@bazariodev/docket/sip';
438
+ import { TEXT_RULES } from '@bazariodev/winnow';
439
+
440
+ const { redactSipMessage } = createSipTools(TEXT_RULES);
441
+ const redactor = createRedactor({}, conversion);
442
+ const message = 'SIP/2.0 200 OK\r\nFrom: "Alice" <sip:alice@example.com>\r\n\r\n';
443
+ const fixed = redactSipMessage(message, { redactor });
444
+ const masked = redactSipMessage(message, {
445
+ redactor,
446
+ redactDisplayName: (value) => redactor.mask(value),
447
+ });
448
+ ```
449
+
450
+ The SIP factory needs five match-only rules: `ipv4`, `ipv6`, `urlCredentials`, `sipUri`, and
451
+ `telUri`. Winnow supplies these rules directly. Other providers must supply replacements that
452
+ use only the matched text. The helper applies each address rule once. The returned SDP rules
453
+ belong only in a SIP-specific policy. Do not add them to the general provider defaults.
454
+
455
+ Without a callback, each non-empty display name becomes `[redacted]`. The callback receives the
456
+ name once, without source quotes and with quoted escapes decoded. It returns plain text. The
457
+ helper escapes quotes and backslashes and writes a quoted name. An empty result removes the name.
458
+ An identity callback, `(value) => value`, keeps the name value. A consumer can pass its injected
459
+ hash function as `redactDisplayName`; the helper does not select `redactor.hash` automatically.
460
+ Other SIP, credential, address, and SDP rules still apply.
461
+
462
+ The callback must return a string synchronously, without control characters, within
463
+ `redactor.limits.maxStringLength`. Invalid results throw; callback errors propagate. Malformed
464
+ identity lists use a fixed replacement without calling the callback. There is no raw-input fallback.
465
+ The helper limits input and complete output to four times `maxStringLength`. Oversized input
466
+ becomes `[truncated]`. Output growth throws `RangeError`. It counts escaped callback output across
467
+ headers before assembly and stops at the limit. Custom callbacks are trusted application code.
468
+
469
+ ## Memory history
470
+
471
+ `createMemoryHistory({ maxRecords })` keeps the newest records in page memory.
472
+ `maxRecords` must be a positive safe integer. It has no default. The only configuration key is
473
+ `maxRecords`; unsupported keys reject instead of silently ignoring retention options.
474
+ At capacity, each write removes the oldest entry before it adds the
475
+ new record. Repeated record IDs remain separate entries. The sink keeps references to the
476
+ redacted, frozen records from Docket core; it does not copy their payloads.
477
+
478
+ `query({ areas?, from?, to? })` captures a snapshot during the call. Bounds are inclusive;
479
+ empty areas select nothing. Results use admission order, including when timestamps move
480
+ backwards. Each query returns a separate array. Later writes and clear do not change it.
481
+ Changing that array does not change history or another snapshot.
482
+ `clear()` immediately releases retained records and permits later writes. It does not change
483
+ logger levels or the upload sink. History has no timer, flush, or stop method. Records remain
484
+ available after the logging hub stops, until clear or page exit.
485
+
486
+ History and upload accept valid immutable `LogRecord` values. They do not validate raw logger input.
487
+ Use Docket core to create, redact, validate, and freeze records before delivery.
488
+
489
+ The default client history keeps the last 1,000 records. It has no duration guarantee or separate
490
+ area budget. At five records per second, 1,000 records cover about 200 seconds. With the default
491
+ `maxRecordSize` of 65,536 characters, retained serialized records can total 65,536,000 characters.
492
+ This is not a heap-byte limit. Copies, snapshots, and archives use additional memory.
493
+
494
+ For separate area budgets, size limits, or age retention, inject a Hopper store:
495
+
496
+ ```ts
497
+ import type { LogRecord } from '@bazariodev/docket';
498
+ import { createMemoryStore } from '@bazariodev/hopper';
499
+
500
+ const retainedHistory = createMemoryStore<LogRecord>({
501
+ maxAgeMs: 3_600_000,
502
+ maxTotalSize: 16_000_000,
503
+ defaultMaxSizePerArea: 1_000_000,
504
+ maxSizePerArea: { application: 4_000_000 },
505
+ maxRecordsPerArea: { application: 20_000 },
506
+ });
507
+ const withAreaBudgets = createDocket({
508
+ areas: [{ id: 'application' }],
509
+ redactor,
510
+ history: retainedHistory,
511
+ });
512
+ ```
513
+
514
+ The application supplies `redactor`. Hopper's IndexedDB history has the same area budgets and
515
+ query filters. It already stores an `area` field and maintains an area/time index. Hopper accepts
516
+ an omitted area for a single stream and uses `default`. Docket records retain their configured areas.
517
+ An age limit permits expiry; it does not guarantee that size or count limits retain a full hour.
518
+
519
+ ### Capacity and input cost
520
+
521
+ - `maxRecords` limits record count, not heap bytes. History and outbox have separate capacities
522
+ and can retain separate copies. Serialization limits do not give an exact heap-size limit.
523
+ - Persistent storage also uses RAM for records, queues, and transaction work.
524
+ - Avoid whole application stores, responses, and large dictionaries as log data. Select a few
525
+ fields at the call site. The field limit does not prevent enumeration of all input keys.
526
+ - Lazy data avoids work when initial admission checks reject a call. An input that passes these
527
+ checks can still be expensive or fail later validation.
528
+ - Use stable scope names. Put variable correlation IDs in context to avoid extra root loggers.
529
+ - Release query results when no longer needed. Old snapshots retain their records after clear.
530
+ - Stop the lifecycle owner and call helper disposers. Client stop does not erase history.
531
+
532
+ ## Periodic upload
533
+
534
+ The application supplies `sendRecords` and `reportUploadError` in this example.
535
+ `sendRecords(records, signal, token)` must accept the complete batch before its promise resolves.
536
+ It owns the endpoint, account credentials, encoding, response checks, and request timeout.
537
+ Use the token as an idempotency key in the account and application scope.
538
+
539
+ ```js
540
+ import { createUploadSink } from '@bazariodev/docket/upload';
541
+ import { createMemoryOutbox } from '@bazariodev/hopper/outbox';
542
+
543
+ const outbox = createMemoryOutbox({ maxRecords: 1000 });
544
+ const upload = createUploadSink({
545
+ maxBatchRecords: 100,
546
+ intervalMs: 15_000,
547
+ send: sendRecords,
548
+ onError: reportUploadError,
549
+ }, outbox);
550
+ const detachUpload = logging.addSink(upload);
551
+ ```
552
+
553
+ The upload sink borrows the outbox. The application owns storage capacity, erasure, and shutdown.
554
+ Use one upload processor per outbox. Each write returns synchronously and starts no request.
555
+ One timer starts an automatic flush after `intervalMs`. Existing pending records also start
556
+ this timer when the processor is created. An idle sink has no timer. Browser suspension can
557
+ delay an attempt. The processor installs no page-exit send. An application-installed page-exit
558
+ helper can call the hub flush and start or resume upload; select local history alone when needed.
559
+
560
+ - `maxBatchRecords`, `intervalMs`, `send`, and the outbox are required. `onError` is optional.
561
+ A supplied observer must be a function.
562
+ - Batch limit and interval must be positive safe integers. The interval cannot exceed
563
+ 2,147,483,647 ms. Neither option has a default.
564
+ - A supplied scheduler needs `setTimeout` and `clearTimeout`. The default uses host timers.
565
+ - The outbox port needs `write`, `capture`, `flush`, `nextBatch`, `acknowledge`, and `getStatus`.
566
+ Docket has no runtime dependency on Hopper. Another adapter can implement this port.
567
+ - Only one send runs at a time. `send` receives a frozen record array, an abort signal,
568
+ and the protected batch token. Resolve only after complete backend acceptance.
569
+ - A failed send or acknowledgement keeps the same logical batch and token for retry.
570
+ Hopper reuses the array within one instance. A recovered batch can exceed a new batch limit.
571
+ After send resolves, the same processor retries a failed acknowledgement without another send.
572
+ A new processor has no such acceptance state and can send that protected token again.
573
+ - Hopper outboxes copy JSON records, preserve FIFO order and duplicate admissions, and protect
574
+ one batch. Overflow removes the oldest unprotected record. If the protected batch fills
575
+ capacity, the incoming record is discarded. Read `droppedRecords` for these losses.
576
+ - `getStatus()` includes the adapter mode, error, record counts, and processor `paused` and `stopped` states.
577
+ A failed or stopped adapter starts no automatic retry. Inspect the error and retry explicitly.
578
+ - A rejected callback pauses automatic attempts. The consumer owns retries inside `send`.
579
+ An explicit `flush()` resumes the retained batch. New writes alone do not resume it.
580
+ - A failed adapter rejects admission. The processor counts each refused record as dropped and
581
+ reports the storage failure once per observed failure episode. Local history keeps recording.
582
+ Processor drops are local to that processor; adapter overflow counts remain in adapter status.
583
+
584
+ Manual `flush()` cancels the timer. It captures the admission boundary before asynchronous work.
585
+ Writes alone do not extend it. A concurrent flush joins the same promise and extends the boundary.
586
+ The first failure rejects that flush. A supplied `onError` observes the failure once. The reporter is not
587
+ awaited; its throws and rejected promises are contained. A flush called synchronously by
588
+ `onError` joins the failed operation. Do not await a flush from a callback that the flush needs.
589
+ Adapter and processor observers report separate operations. Select one reporting path or deduplicate
590
+ them in the application when the same failure reaches both. Repeated explicit retries can report
591
+ new attempt failures. Keep a status observer or status checks active when using background upload.
592
+
593
+ For persistent pending records, replace only the adapter:
594
+
595
+ ```js
596
+ import { createIndexedDbOutbox } from '@bazariodev/hopper/outbox/indexeddb';
597
+
598
+ const outbox = await createIndexedDbOutbox({
599
+ ...storageOptions.outbox,
600
+ maxRecords: 1000,
601
+ onError: reportUploadError,
602
+ });
603
+ ```
604
+
605
+ The asynchronous factory restores records and their protected batch before it returns.
606
+ Supply required `databaseName` and `lockName` values in the initial options. The adapter uses
607
+ the supplied lock name unchanged. All writers of one database must use that same lock name.
608
+ Missing or invalid names reject before platform access.
609
+ It requires IndexedDB and exclusive Web Locks ownership. It rejects unavailable storage,
610
+ ownership, and incompatible schemas. It does not silently switch to memory.
611
+ Use a separate database for download history. No database is accessed by the memory adapter.
612
+
613
+ Outbox `flush()` means storage completion. Upload `flush()` also waits for backend acceptance
614
+ and local acknowledgement. Delivery is at least once. A lost response or a new processor after
615
+ failed acknowledgement can cause a duplicate send. Storage removal, overflow, and page exit before storage commit can
616
+ still lose records. No adapter provides exactly-once delivery.
617
+
618
+ Batch limits count records, not encoded request bytes. The client's default 100-record batch can contain
619
+ 6,553,600 serialized characters before request framing. Choose a smaller `maxBatchRecords` for
620
+ large records. The consumer can split one protected batch into bounded requests; resolve only
621
+ after every part is accepted. Use a stable, distinct deduplication key for each part.
622
+
623
+ A permanent rejection must not silently become acceptance. In particular, do not treat every
624
+ 4xx response as success. The consumer can stop the processor, inspect the protected batch, report
625
+ an intentional discard, then acknowledge that exact token. `outbox.clear()` discards all pending
626
+ records. Both are application data-loss decisions. Authentication failures must not switch a batch
627
+ to another account's credentials.
628
+
629
+ ## Export and download
630
+
631
+ ```js
632
+ import { createLogBundle } from '@bazariodev/docket/export';
633
+ import { downloadBlob } from '@bazariodev/docket/browser';
634
+ import * as archive from '@bazariodev/baler';
635
+
636
+ const bundle = await createLogBundle({
637
+ source: history,
638
+ redactor,
639
+ files: [{ name: 'application.log', areas: ['application', 'network'] }],
640
+ }, archive);
641
+ downloadBlob(bundle.blob, bundle.fileName);
642
+ ```
643
+
644
+ The bundle uses one fixed snapshot while recording continues. Download neither drains history
645
+ nor acknowledges upload records. It works without an uploader and during upload failure.
646
+ The manifest uses the minimum and maximum selected timestamps for its actual range. Returned
647
+ snapshots and files belong to their caller. Clear cannot erase those returned values.
648
+
649
+ `maxInputSize` defaults to 32,000,000 UTF-8 bytes across all files and the manifest.
650
+ The exporter checks each formatted record, including its trailing newline, before retaining it.
651
+ It checks custom output on return and checks the serialized manifest before archive assembly.
652
+ Overflow rejects with `RangeError` and stops later rendering and archive work. The archive
653
+ provider also checks the final input size.
654
+
655
+ This limit bounds retained export text, not peak heap use. The source can allocate its snapshot
656
+ before formatting. One current record, a custom renderer, or manifest serialization can allocate
657
+ before its size check. Synchronous custom rendering cannot be interrupted.
658
+
659
+ ### JSON and NDJSON
660
+
661
+ Use queried records directly for structured output. They have already passed redaction:
662
+
663
+ ```js
664
+ const records = await history.query();
665
+ const json = JSON.stringify(records);
666
+ const ndjson = records.map((record) => `${JSON.stringify(record)}\n`).join('');
667
+ ```
668
+
669
+ These application-owned strings have no bundle byte limit. Bound the selected history and
670
+ release snapshots and strings when no longer needed. Do not add raw input after redaction.
671
+
672
+ ## Shutdown and account changes
673
+
674
+ `createDocket().flush()` waits for its sinks, including delivery through a configured uploader.
675
+ Its `stop()` cancels upload before sink cleanup and starts no final send. Flush explicitly
676
+ before stop when application policy requires delivery. Rejection pauses automatic upload;
677
+ another explicit flush resumes it. The consumer owns request retry and timeout policy.
678
+
679
+ Browser helpers have separate disposers. This example reuses the memory client from Usage:
680
+
681
+ ```js
682
+ import { captureGlobalErrors } from '@bazariodev/docket/browser';
683
+
684
+ const disposeErrors = captureGlobalErrors(
685
+ docket.getLogger('application', 'Global'),
686
+ );
687
+ try {
688
+ await docket.flush();
689
+ } finally {
690
+ disposeErrors();
691
+ await docket.stop();
692
+ }
693
+ ```
694
+
695
+ Stop preserves history. Use the [account cleanup sequence below](#account-cleanup)
696
+ when application policy requires erasure.
697
+
698
+ `logging.stop()` closes admission, flushes each sink, then stops each sink. The uploader's flush
699
+ includes retained work when it joins an active operation. A failure still runs sink cleanup and
700
+ reaches the hub's aggregate error. History remains available for download.
701
+
702
+ ### Account cleanup
703
+
704
+ For logout, stop sending before the hub can flush the uploader. Then erase and close storage:
705
+
706
+ ```js
707
+ const failures = [];
708
+ for (const step of [
709
+ () => logging.resetLevel(),
710
+ () => upload.stop(),
711
+ () => outbox.clear(),
712
+ () => logging.stop(),
713
+ () => outbox.stop(),
714
+ () => history.clear(),
715
+ ]) {
716
+ try {
717
+ await step();
718
+ } catch (error) {
719
+ failures.push(error);
720
+ }
721
+ }
722
+ if (failures.length) throw new AggregateError(failures, 'Cleanup failed');
723
+ ```
724
+
725
+ For shutdown that preserves pending records, stop the uploader, then stop the outbox.
726
+ Upload stop cancels its timer, signals the active send, and starts no final send or retry.
727
+ It resolves without waiting for an uncooperative send. An interrupted flush rejects with
728
+ `AbortError`. Late send settlement cannot start an acknowledgement or restart processing.
729
+ An acknowledgement started before stop can still complete. Store clear and stop wait for
730
+ storage work. Upload stop neither clears nor closes the borrowed adapter.
731
+ Writes after processor stop are ignored. Processor flush after stop resolves without sending.
732
+
733
+ `detachUpload()` prevents future hub delivery. The application must still stop the uploader.
734
+ The application owns account changes, credentials, logger-level resets, and cancellation of
735
+ outstanding downloads. Bind each uploader to its original account; stop it before credentials
736
+ change, then create a new uploader. Local clear does not stop or flush upload.
737
+
738
+ `createLogHistory` and its account/session types are removed. The replacement has no account
739
+ controller or automatic legacy cleanup. The memory helper never accesses disk history.
740
+ Deployed consumers need a separate migration for legacy storage, including prior memory mode.
741
+ Switching to memory does not erase that storage. Standalone `/indexeddb` remains available.
742
+
743
+ TypeScript imports from the former `/history` entry also change:
744
+
745
+ | Former type | Current entry |
746
+ | --- | --- |
747
+ | `CopyRecordsOptions` | `@bazariodev/hopper` |
748
+ | `LogOverflowStrategy`, `LogStoreError`, `MemoryLogStoreOptions` | `@bazariodev/docket/indexeddb` |
749
+
750
+ Use the new `/history` types only for FIFO memory history. Hopper exposes its generic store
751
+ types directly; Docket's `/indexeddb` keeps its logging-specific compatibility types.
752
+
753
+ ### Migrate storage from 0.0.0
754
+
755
+ The old managed history could create an empty database and owner metadata during account
756
+ transitions, even in memory mode. Use the original database override if one was supplied.
757
+ Otherwise its names were:
758
+
759
+ | Resource | Legacy name |
760
+ | --- | --- |
761
+ | Managed database | `bazario-logging:history:${leaseKey}` |
762
+ | Managed localStorage metadata | `bazario-logging:metadata:${databaseName}` |
763
+ | Managed or standalone database lock | `JSON.stringify(['bazario:logging:indexeddb', databaseName])` |
764
+ | Standalone database without an override | `bazario-logging` |
765
+
766
+ The metadata can contain a salt, full owner digest, level overrides, and a persistent-mode key.
767
+ Remove it as well as the database when that legacy history is no longer needed. Stop old clients
768
+ first. A new Hopper default lock does not exclude a live 0.0.0 writer; supply the exact legacy lock.
769
+ Keep this lock protocol until all writers of that database have migrated. Use separate names
770
+ for a new account schema.
771
+
772
+ The application owns permanent erasure of its dedicated database. Acquire its exact exclusive
773
+ lock with `ifAvailable: true`; refuse when it is held. Under that lock, call
774
+ `indexedDB.deleteDatabase(databaseName)` and await native success or error. After success, call
775
+ `localStorage.removeItem(metadataKey)` for the managed history key. Do not remove unrelated
776
+ origin data. Deletion creates no database and needs no compatible schema or metadata writes.
777
+ The same path can erase a corrupt dedicated outbox when schema-preserving clear cannot proceed.
778
+
779
+ Native deletion cannot be canceled. If another connection blocks it, close old tabs or connections
780
+ and keep the lock until deletion settles. A timeout must not release ownership while deletion
781
+ remains pending. Never report completed erasure before native success. See
782
+ [MDN deleteDatabase](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/deleteDatabase).
783
+ The repository's consumer example supplies `eraseDiagnosticStorage` for this operation;
784
+ it is application wiring, not a Docket export.
785
+
786
+ ## Persistent storage
787
+
788
+ Use a Hopper store directly as a sink and an export source. Both Hopper history adapters
789
+ sort by timestamp, run ID, and sequence. They keep the first retained `(runId, seq)` record.
790
+ This differs from the FIFO helper, which preserves duplicate admissions.
791
+
792
+ ```js
793
+ import { createIndexedDbStore } from '@bazariodev/hopper/indexeddb';
794
+
795
+ const history = createIndexedDbStore({
796
+ ...storageOptions.history,
797
+ });
798
+ const detachHistory = logging.addSink(history);
799
+ // Pass history as the source for createLogBundle.
800
+ await history.flush();
801
+ const status = history.getStatus();
802
+ ```
803
+
804
+ `/indexeddb` is an optional structural adapter. Supply Hopper's factory as its second argument.
805
+ Both `databaseName` and `lockName` are required. The adapter forwards them unchanged and rejects
806
+ missing or invalid names before calling the provider. It supplies no database or lock default.
807
+
808
+ The supplied Hopper store first tries an exclusive database lock. A blocked or unavailable lock selects memory
809
+ fallback for that instance's lifetime. It cannot later replay that fallback into another owner's
810
+ history. A new instance can try ownership again. With `allowUnprotected: true`, failed acquisition
811
+ permits IndexedDB access without a lease. The store still tries the lock first.
812
+
813
+ Unprotected access cannot prevent another writer from recording after clear. It cannot establish
814
+ stable account history. Read `unprotected` and `unavailableHistory` from status after an operation.
815
+ Status reads are synchronous; initial acquisition is asynchronous.
816
+
817
+ An open or write failure with permitted access starts bounded memory fallback. Recovery retries
818
+ every ten minutes. A failed write batch is lost and its flush rejects. Other pending records move
819
+ to fallback. Successful recovery removes copied fallback records only after commit.
820
+
821
+ `clearIndexedDbLogStore(options, clearStore)` supplies the same logging database and lock names
822
+ to a standalone cleanup provider. With Hopper `clearIndexedDbStore`, it clears existing records
823
+ and metadata without creating a database. Listing can still access IndexedDB. The new memory
824
+ entries never call this cleanup path.
825
+
826
+ ## Behavior
827
+
828
+ `isActive` is an optional admission guard. Returning `false` stops a record before conversion or
829
+ delivery. The hub checks it again after application callbacks. `createDocket` captures this
830
+ function from the original options, including prototype methods, and calls it with that options object as
831
+ its receiver. State changes on the original object take effect on later checks. A replacement
832
+ method does not replace the captured gate. If the gate stops the client, the current record is
833
+ blocked before delivery. The application supplies this guard when it needs one.
834
+
835
+ - Levels are `debug`, `info`, `warn`, and `error`. Default area level is `info`.
836
+ - Data functions run only after admission. No sinks, stop, levels, and storm limits can prevent admission.
837
+ - The first 1,000 root loggers are cached by area and scope. Further pairs are uncached; one
838
+ `scope` diagnostic reports the limit. Put correlation IDs in child context.
839
+ - Each record is redacted once, then validated, copied, and deeply frozen before sink delivery.
840
+ - Custom redactors must return JSON with dense arrays. Invalid output is dropped and reports `redactor`.
841
+ - Conversion does not call getters or `toJSON`. It bounds strings, structure, and serialized size.
842
+ - Error causes and aggregate errors become structured data. Non-finite numbers become null.
843
+ - Console and default file text include custom Error fields as compact JSON. Their keys and
844
+ values use the same control-character escaping as other Error details.
845
+ - Record validation checks fixed record and Error fields separately from application data. The full
846
+ serialized record must fit `maxRecordSize`. Fixed level values remain intact at small text limits.
847
+ - Default storm allowance is 100 records per area/scope each second. One timer reports suppression.
848
+ - Sink failures do not stop other sinks. Recursive logging from sinks and reporters is rejected.
849
+
850
+ The standalone store defaults and recovery rules below describe the Hopper provider.
851
+ They do not apply to the FIFO history helper or outbox adapters.
852
+ Other providers must state their supported behavior.
853
+
854
+ - Queries use inclusive time bounds and order by time, run ID, then sequence. Empty areas select nothing.
855
+ - Both stores keep the first retained `(runId, seq)` record. Eviction releases its identity.
856
+ - Default budgets are 1,000,000 JSON characters per area, 16,000,000 total, and 24-hour age.
857
+ - IndexedDB also defaults to 20,000 records per area, 250 records per batch, and a one-second flush interval.
858
+ - IndexedDB recovery uses oldest-record eviction for copied and queued fallback records. This replay does not call `onRotate`.
859
+ - Pending plus active database records are limited to 1,000 records and 4,000,000 JSON characters.
860
+ - Expired data is excluded from queries. Disk expiry cleanup runs at open and write, without a timer.
861
+ - `flush()` means local transaction commit. Page exit is best effort; crash survival is not guaranteed.
862
+ - Detach removes sink delivery. The sink's lifecycle owner must stop it separately.
863
+ - A stopped store ignores writes. Its query and clear remain available under their access rules.
864
+
865
+ ## API
866
+
867
+ | Entry | Exports |
868
+ | --- | --- |
869
+ | Root | `createLogging`, `createConsoleSink`, `formatRecord` |
870
+ | `/client` | `createDocket(options)` |
871
+ | `/redaction` | `createRedactor(policy, providers)` |
872
+ | `/indexeddb` | `createIndexedDbLogStore(options, createStore)`, `clearIndexedDbLogStore(options, clearStore)` |
873
+ | `/history` | `createMemoryHistory(options)` |
874
+ | `/upload` | `createUploadSink(options, outbox)` |
875
+ | `/export` | `createLogBundle(options, archive)` |
876
+ | `/browser` | `captureGlobalErrors`, `flushOnPageHide`, `collectBrowserEnvironment`, `downloadBlob` |
877
+ | `/sip` | `createSipTools(textRules)`, `describeSipMessage`. The tools expose `redactSipMessage`, `describeSipMessage`, and `SDP_TEXT_RULES`. |
878
+ | `/devtools` | `installConsoleApi` |
879
+
880
+ | Component | Options and methods |
881
+ | --- | --- |
882
+ | Docket client | Hub options plus `history` and `upload`. Returns hub methods, `history`, `outbox`, and `upload`. Disabled features are `undefined`. Stop cancels sending before storage shutdown. |
883
+ | Hub | Required `areas`, `redactor`; optional `sinks`, `context`, `levelStorage`, `maxOverrideMs`, `stormLimit`, `scheduler`, clocks, `createRunId`, `onError`, `isActive`. Methods: `getLogger`, `setContext`, `setLevel`, `resetLevel`, `reloadLevels`, `getLevels`, `addSink`, `flush`, `stop`. |
884
+ | Logger | `debug`, `info`, `warn`, `error` accept `(message, data?)`. `child({ scope?, context? })` adds bindings. |
885
+ | Redactor | Policy: `version`, `hash`, `fields`, `unknownFields`, `text`, `limits`. Methods: `redact`, `redactValue`, `redactText`, `mask`, optional `hash`. Resolved `limits` and `version` are public. |
886
+ | Memory store | `maxSizePerArea`, `defaultMaxSizePerArea`, `maxTotalSize`, `maxAgeMs`, `now`, `overflowStrategy`, `onRotate`, `scheduler`, `onError`. Methods: `write`, `query`, `flush`, `clear`, `stop`, `getStatus`. |
887
+ | IndexedDB store | Memory options plus required `databaseName` and `lockName`, optional `indexedDB`, `locks`, `allowUnprotected`, batch/queue/interval/count limits, and `fallback`. |
888
+ | Memory history | Required `maxRecords`. Queue creation is internal. Methods: `write`, `query`, `clear`. |
889
+ | Upload sink | Required `maxBatchRecords`, `intervalMs`, `send`, and an outbox. Optional `onError` and `scheduler`. Methods: `write`, `flush`, `stop`, `getStatus`. |
890
+ | Bundle | Required `source`, `redactor`, `files`, and the separate archive capability. Optional time bounds, `manifest`, `fileName`, `maxInputSize`, clock, scheduler. Returns `blob`, `fileName`, file counts. |
891
+ | Supplied Hopper copy helper | `copyRecords(from, to, { maxChunkRecords?, maxChunkSize? })`; defaults are 100 records and 1,000,000 JSON characters. It flushes between chunks. |
892
+ | Console sink | Optional minimum `level` and injected `console`. |
893
+ | Custom sink | Implement `LogSink.write(record)`. Optional `flush` and `stop` return promises. |
894
+ | Console API | Inject level controls and optional `clear` and `exportBundle`. Optional `name`, `target`. Returns cleanup. |
895
+
896
+ Redaction defaults: depth 6; strings 16,384 characters; arrays 100 entries; objects 100 keys;
897
+ 1,000 nodes; 65,536 JSON characters per record. Field rules are case-insensitive at every depth:
898
+ `keep`, `drop`, `mask`, `hash`. Unknown fields default to keep. Hash rules require an injected hash.
899
+ An explicit `text: []` disables default text rules. Global non-sticky regular expressions are required.
900
+ Custom patterns and callbacks are trusted application code.
901
+
902
+ SIP redaction preserves protocol structure and replaces identity and credential values. Unknown
903
+ SIP bodies become content-type and byte-count markers. The application owns any selected hasher
904
+ and its dependency. The package does not implement a cryptographic algorithm.
905
+
906
+ Bundle filenames cannot contain paths, reserved device names, or manifest variants. Comparison
907
+ uses NFC and ignores case. Empty files are omitted; the manifest retains their zero counts.
908
+ File names, each file's area list, and the download name are checked before the source query. Its default timestamp is
909
+ the call's start time. The manifest build time is recorded after rendering.
910
+ Custom renderers are synchronous. Built-in formatting yields during large files. The
911
+ [export byte limit](#export-and-download) applies during formatting and before compression.
912
+ The supplied Baler implementation uses raw deflate when available and stored entries otherwise.
913
+ The manifest contains ranges, counts, truncation, warnings, policy versions, store status, and
914
+ redacted application metadata. It contains no history key.
915
+
916
+ `formatRecord` produces human-readable text with flat labels. Context and field keys can repeat
917
+ header names such as `runId` or `message`. Choose distinct data names and use structured records
918
+ for machine parsing; the text format does not guarantee unique keys across those sections.
919
+
920
+ Browser listeners are explicit and return cleanup. Environment defaults are language, UTC offset,
921
+ display mode, and online state. Extra fields need explicit selection. Optional reads use a 500 ms
922
+ deadline by default. Unknown field names reject before platform reads. Collection never requests capture or permission and omits device IDs and labels.
923
+
924
+ External service integrations follow the [synchronous sink contract](#console-output-and-custom-sinks).
925
+ The application owns service mapping, network delivery, and SDK lifecycle.
926
+
927
+ ## Privacy
928
+
929
+ Records contain time, run ID, sequence, area, scope, level, message, and selected context, data,
930
+ and Error fields after the configured redactor runs. Application policy determines which private
931
+ values remain. A run ID is a pseudonym, not an account permission or a guarantee of anonymity.
932
+ Do not use direct account identifiers in database names. The application supplies an opaque scope.
933
+
934
+ Default history stays in page memory until eviction, clear, or page exit. A memory outbox also loses
935
+ records on reload. IndexedDB history and outboxes are device storage and can survive restart.
936
+ History follows its budgets. Outbox records remain until acknowledgement, clear, or capacity loss;
937
+ outboxes have no age expiry. Stop preserves persistent records. Clear cannot erase exported files,
938
+ returned snapshots, records already sent, or copies held by application code.
939
+
940
+ Upload is disabled unless configured. The consumer selects the endpoint and every third party,
941
+ owns credentials, retry, backend retention, access, and deletion. The application must apply its
942
+ permission policy separately to local recording and background upload. Enabling one does not
943
+ grant permission for the other. Docket adds no consent UI or external service.
944
+
945
+ Delivery is at least once. The backend can receive duplicates and must deduplicate batch tokens
946
+ within the original account scope. Keep account databases and sender credentials separate after
947
+ forced sign-out, failed cleanup, and restart. An old account's records must never use a new
948
+ account's sender. Retry failed cleanup under the old account's known resource names.
949
+
950
+ ## Errors
951
+
952
+ Logger writes never throw. Configuration errors and missing capabilities can throw `TypeError`. The hub reports sources
953
+ `data`, `redactor`, `sink`, `storage`, `scope`, and `reentry` through `onError`. Flush and stop attempt
954
+ all sinks and reject with an aggregate error on failure. Hub-generated diagnostics use codes
955
+ `recursive-logging`, `root-logger-cache-limit`, and `stopped`. Callback failures keep their original errors.
956
+
957
+ Standalone store errors have codes `open`, `write`, `read`, `quota`, `overflow`, `ownership`,
958
+ `clear`, `upgrade`, and `rotation`. Memory history rejects invalid query filters with `TypeError`.
959
+ Invalid memory-history options throw `TypeError`. The upload factory validates its options and
960
+ outbox before it schedules or sends work.
961
+
962
+ Upload flush rejects on the first storage, send, or acknowledgement failure.
963
+ A supplied `onError` receives each failed attempt once,
964
+ including an adapter `AbortError` while the sink is active. Sink stop instead rejects interrupted
965
+ flushes with `AbortError` without reporting an upload failure. Export and developer actions reject
966
+ to their caller. These failures do not remove retained local history.
967
+
968
+ ## TypeScript
969
+
970
+ Declarations are provided for ESM and CommonJS. Core, memory history, redaction, SIP, and devtools
971
+ need only ES2022 types. Upload and client need `AbortSignal` declarations from the target runtime.
972
+ Browser, persistence, and export entries use DOM types. No entry declaration imports a provider
973
+ package. The logger satisfies the existing workspace logger ports without an adapter.
974
+ Area inference rejects unknown area names.
975
+ Import `DocketOptions`, `Docket`, `HistoryClient`, `OutboxClient`, and `DocketUploadOptions` from
976
+ `@bazariodev/docket/client`. Hopper clients satisfy these local structural types.
977
+ Import `MemoryHistoryOptions` and `MemoryHistory` from `@bazariodev/docket/history`. Import
978
+ `UploadSinkOptions`, `UploadSinkStatus`, `UploadSink`, and `UploadOutbox` from
979
+ `@bazariodev/docket/upload`. Hopper's `Outbox<LogRecord>` satisfies the upload port.
980
+ Queue types are internal. Consumers do not supply or import them for memory history.
981
+ Import `LogRecord` from `@bazariodev/docket`. Use `createMemoryStore<LogRecord>` when selecting
982
+ Hopper's standalone store or rotation snapshots. The export source receives a query argument.
983
+
984
+ ## License
985
+
986
+ [MIT](./LICENSE)