@tinycloud/sdk-services 3.2.6-beta.2 → 3.2.6-beta.4

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 (59) hide show
  1. package/dist/{BaseService-4wwJq1Gn.d.cts → BaseService-1Ug6y7DC.d.cts} +1 -1
  2. package/dist/{BaseService-BGr2slUz.d.ts → BaseService-Bn07Vudy.d.ts} +1 -1
  3. package/dist/encryption/index.cjs +2 -0
  4. package/dist/encryption/index.cjs.map +1 -1
  5. package/dist/encryption/index.d.cts +2 -2
  6. package/dist/encryption/index.d.ts +2 -2
  7. package/dist/encryption/index.js +2 -0
  8. package/dist/encryption/index.js.map +1 -1
  9. package/dist/index.cjs +15 -7
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +14 -11
  12. package/dist/index.d.ts +14 -11
  13. package/dist/index.js +15 -7
  14. package/dist/index.js.map +1 -1
  15. package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
  16. package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
  17. package/dist/kv/index.cjs +2 -0
  18. package/dist/kv/index.cjs.map +1 -1
  19. package/dist/kv/index.d.cts +7 -4
  20. package/dist/kv/index.d.ts +7 -4
  21. package/dist/kv/index.js +2 -0
  22. package/dist/kv/index.js.map +1 -1
  23. package/dist/kv/replication/contractConstants.cjs +115 -0
  24. package/dist/kv/replication/contractConstants.cjs.map +1 -0
  25. package/dist/kv/replication/contractConstants.d.cts +77 -0
  26. package/dist/kv/replication/contractConstants.d.ts +77 -0
  27. package/dist/kv/replication/contractConstants.js +75 -0
  28. package/dist/kv/replication/contractConstants.js.map +1 -0
  29. package/dist/kv/replication/grantPolicyConstants.cjs +42 -0
  30. package/dist/kv/replication/grantPolicyConstants.cjs.map +1 -0
  31. package/dist/kv/replication/grantPolicyConstants.d.cts +10 -0
  32. package/dist/kv/replication/grantPolicyConstants.d.ts +10 -0
  33. package/dist/kv/replication/grantPolicyConstants.js +14 -0
  34. package/dist/kv/replication/grantPolicyConstants.js.map +1 -0
  35. package/dist/kv/replication/grantPolicyTypes.cjs +19 -0
  36. package/dist/kv/replication/grantPolicyTypes.cjs.map +1 -0
  37. package/dist/kv/replication/grantPolicyTypes.d.cts +87 -0
  38. package/dist/kv/replication/grantPolicyTypes.d.ts +87 -0
  39. package/dist/kv/replication/grantPolicyTypes.js +1 -0
  40. package/dist/kv/replication/grantPolicyTypes.js.map +1 -0
  41. package/dist/kv/replication/index.cjs +5 -4
  42. package/dist/kv/replication/index.cjs.map +1 -1
  43. package/dist/kv/replication/index.d.cts +6 -3
  44. package/dist/kv/replication/index.d.ts +6 -3
  45. package/dist/kv/replication/index.js +5 -4
  46. package/dist/kv/replication/index.js.map +1 -1
  47. package/dist/scope-BnyVPZ5E.d.ts +1659 -0
  48. package/dist/scope-CQ5kmHlt.d.cts +1659 -0
  49. package/dist/sql/index.cjs +2 -0
  50. package/dist/sql/index.cjs.map +1 -1
  51. package/dist/sql/index.d.cts +2 -2
  52. package/dist/sql/index.d.ts +2 -2
  53. package/dist/sql/index.js +2 -0
  54. package/dist/sql/index.js.map +1 -1
  55. package/dist/{types-D-9A3Sut.d.cts → types-gmdUiorz.d.cts} +2 -0
  56. package/dist/{types-D-9A3Sut.d.ts → types-gmdUiorz.d.ts} +2 -0
  57. package/package.json +40 -1
  58. package/dist/scope-CoKKmMiI.d.ts +0 -957
  59. package/dist/scope-D7wmjbMH.d.cts +0 -957
@@ -0,0 +1,1659 @@
1
+ import { R as Result, F as FetchFunction } from './types-gmdUiorz.cjs';
2
+ import { REPLICA_KV_STORES } from './kv/replication/contractConstants.cjs';
3
+ import { ReplicaConsentAnchor, ReplicaGrantPolicy } from './kv/replication/grantPolicyTypes.cjs';
4
+
5
+ /**
6
+ * KV Service Types
7
+ *
8
+ * Type definitions for the KV (Key-Value) service operations.
9
+ */
10
+
11
+ /**
12
+ * Configuration for KVService.
13
+ */
14
+ interface KVServiceConfig {
15
+ /**
16
+ * Default prefix for all keys.
17
+ * Useful for namespacing data within a space.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const kv = new KVService({ prefix: 'myapp/settings' });
22
+ * await kv.put('theme', 'dark'); // Stores at 'myapp/settings/theme'
23
+ * ```
24
+ */
25
+ prefix?: string;
26
+ /**
27
+ * Default timeout in milliseconds for each KV request, including reading
28
+ * the response body. When it elapses the request is aborted and the
29
+ * operation returns an `ErrorCodes.TIMEOUT` error. A per-call `timeout`
30
+ * option overrides it. Unset (the default), `0`, or `Infinity` means no
31
+ * timeout.
32
+ */
33
+ timeout?: number;
34
+ /** Allow additional config properties */
35
+ [key: string]: unknown;
36
+ }
37
+ /**
38
+ * Options for KV get operations.
39
+ */
40
+ interface KVGetOptions {
41
+ /**
42
+ * Override the default prefix for this operation.
43
+ */
44
+ prefix?: string;
45
+ /**
46
+ * Require this read to bypass replication and use the network directly.
47
+ * A configured read-through observer receives `NETWORK_REQUESTED` after the
48
+ * network result settles, with its outcome and latency. This is supported by
49
+ * `get` and `list`; `head`, `batchGet`, `batchHead` and signed URL operations
50
+ * always use the network.
51
+ */
52
+ source?: "network";
53
+ /**
54
+ * Return raw response instead of parsed JSON.
55
+ * When true, data will be the raw response text.
56
+ */
57
+ raw?: boolean;
58
+ /**
59
+ * Return the raw response bytes as a Uint8Array instead of parsed/text data.
60
+ * Use this to read back binary values (e.g. images) byte-identically. Takes
61
+ * precedence over {@link raw}.
62
+ */
63
+ binary?: boolean;
64
+ /**
65
+ * Ask the node to reject the response when the stored value exceeds this
66
+ * number of bytes. This bounds downloads before the response body is read.
67
+ */
68
+ maxResponseBytes?: number;
69
+ /**
70
+ * Custom timeout for this operation in milliseconds. Overrides
71
+ * `KVServiceConfig.timeout`; `0` disables a configured timeout.
72
+ */
73
+ timeout?: number;
74
+ /**
75
+ * Custom abort signal for this operation.
76
+ */
77
+ signal?: AbortSignal;
78
+ }
79
+ /**
80
+ * Options for KV put operations.
81
+ */
82
+ interface KVPutOptions {
83
+ /**
84
+ * Override the default prefix for this operation.
85
+ */
86
+ prefix?: string;
87
+ /**
88
+ * Content type for the value.
89
+ * Defaults to 'application/json' for objects.
90
+ */
91
+ contentType?: string;
92
+ /**
93
+ * Custom metadata headers to store with the value.
94
+ */
95
+ metadata?: Record<string, string>;
96
+ /** Only write when the current object has this strong ETag. */
97
+ ifMatch?: string;
98
+ /** Only write when the key does not exist. */
99
+ ifNoneMatch?: "*";
100
+ /**
101
+ * Custom timeout for this operation in milliseconds. Overrides
102
+ * `KVServiceConfig.timeout`; `0` disables a configured timeout.
103
+ */
104
+ timeout?: number;
105
+ /**
106
+ * Custom abort signal for this operation.
107
+ */
108
+ signal?: AbortSignal;
109
+ }
110
+ /**
111
+ * One entry in a KV batch put request.
112
+ */
113
+ interface KVBatchPutItem {
114
+ /**
115
+ * The key to store under.
116
+ */
117
+ key: string;
118
+ /**
119
+ * The value to store.
120
+ *
121
+ * Objects are JSON stringified. Strings are stored as text. Binary values
122
+ * should be supplied as Blob, ArrayBuffer, or Uint8Array.
123
+ */
124
+ value: unknown;
125
+ /**
126
+ * Content type for this item. Defaults to application/json for objects and
127
+ * application/octet-stream for binary values.
128
+ */
129
+ contentType?: string;
130
+ }
131
+ /**
132
+ * Options for KV batch put operations.
133
+ */
134
+ interface KVBatchPutOptions {
135
+ /**
136
+ * Override the default prefix for all entries in this batch.
137
+ */
138
+ prefix?: string;
139
+ /**
140
+ * Custom timeout for this operation in milliseconds. Overrides
141
+ * `KVServiceConfig.timeout`; `0` disables a configured timeout.
142
+ */
143
+ timeout?: number;
144
+ /**
145
+ * Custom abort signal for this operation.
146
+ */
147
+ signal?: AbortSignal;
148
+ }
149
+ /**
150
+ * Response from KV batch put operations.
151
+ */
152
+ interface KVBatchPutResponse {
153
+ /**
154
+ * Keys successfully written by the batch.
155
+ */
156
+ written: string[];
157
+ /**
158
+ * Number of written keys.
159
+ */
160
+ count: number;
161
+ }
162
+ /**
163
+ * Response from a KV batch read. Transport or authorization failures are
164
+ * returned by the outer Result; each requested key has its own Result so a
165
+ * missing key does not discard successful siblings.
166
+ */
167
+ interface KVBatchReadResponse<T = unknown> {
168
+ results: Array<{
169
+ key: string;
170
+ result: Result<KVResponse<T>>;
171
+ }>;
172
+ count: number;
173
+ }
174
+ /**
175
+ * Options for KV list operations.
176
+ */
177
+ interface KVListOptions {
178
+ /**
179
+ * Override the default prefix for this operation.
180
+ */
181
+ prefix?: string;
182
+ /**
183
+ * Require this read to bypass replication and use the network directly. A
184
+ * configured read-through observer receives `NETWORK_REQUESTED` after the
185
+ * network result settles, with its outcome and latency.
186
+ */
187
+ source?: "network";
188
+ /**
189
+ * Additional path to append to the prefix.
190
+ */
191
+ path?: string;
192
+ /**
193
+ * Whether to remove the prefix from returned keys.
194
+ * When true, keys are returned relative to the prefix.
195
+ */
196
+ removePrefix?: boolean;
197
+ /**
198
+ * Return raw response instead of parsed JSON.
199
+ */
200
+ raw?: boolean;
201
+ /** Maximum number of keys the node may return. */
202
+ limit?: number;
203
+ /** Opaque scope-bound cursor returned by a previous bounded list. */
204
+ cursor?: string;
205
+ /**
206
+ * Custom timeout for this operation in milliseconds. Overrides
207
+ * `KVServiceConfig.timeout`; `0` disables a configured timeout.
208
+ */
209
+ timeout?: number;
210
+ /**
211
+ * Custom abort signal for this operation.
212
+ */
213
+ signal?: AbortSignal;
214
+ }
215
+ /**
216
+ * Options for KV delete operations.
217
+ */
218
+ interface KVDeleteOptions {
219
+ /**
220
+ * Override the default prefix for this operation.
221
+ */
222
+ prefix?: string;
223
+ /** Only delete when the current object has this strong ETag. */
224
+ ifMatch?: string;
225
+ /**
226
+ * Custom timeout for this operation in milliseconds. Overrides
227
+ * `KVServiceConfig.timeout`; `0` disables a configured timeout.
228
+ */
229
+ timeout?: number;
230
+ /**
231
+ * Custom abort signal for this operation.
232
+ */
233
+ signal?: AbortSignal;
234
+ }
235
+ /**
236
+ * Options for KV head (metadata) operations.
237
+ */
238
+ interface KVHeadOptions {
239
+ /**
240
+ * Override the default prefix for this operation.
241
+ */
242
+ prefix?: string;
243
+ /**
244
+ * Custom timeout for this operation in milliseconds. Overrides
245
+ * `KVServiceConfig.timeout`; `0` disables a configured timeout.
246
+ */
247
+ timeout?: number;
248
+ /**
249
+ * Custom abort signal for this operation.
250
+ */
251
+ signal?: AbortSignal;
252
+ }
253
+ /**
254
+ * Default lifetime for signed KV read URLs when a caller omits expiresInSeconds.
255
+ * SDK duration defaults are stored in milliseconds; createSignedReadUrl converts
256
+ * this to the node endpoint's ttl_seconds field.
257
+ *
258
+ * Keep this in sync with EXPIRY.SIGNED_READ_URL_MS in @tinycloud/sdk-core.
259
+ * sdk-services cannot import sdk-core because sdk-core depends on sdk-services.
260
+ */
261
+ declare const DEFAULT_SIGNED_READ_URL_EXPIRY_MS: number;
262
+ /**
263
+ * Options for creating a signed KV read URL.
264
+ */
265
+ interface KVCreateSignedReadUrlOptions {
266
+ /**
267
+ * Override the default prefix for this operation.
268
+ */
269
+ prefix?: string;
270
+ /**
271
+ * Requested URL lifetime in seconds.
272
+ * Defaults to {@link DEFAULT_SIGNED_READ_URL_EXPIRY_MS} converted to seconds.
273
+ * The node may cap this by its configured maximum, the invocation expiry,
274
+ * or the parent delegation expiry.
275
+ */
276
+ expiresInSeconds?: number;
277
+ /**
278
+ * Optional blake3 content hash to bind the signed URL to a specific object.
279
+ */
280
+ contentHash?: string;
281
+ /**
282
+ * Optional ETag to bind the signed URL to a specific object version.
283
+ */
284
+ etag?: string;
285
+ /**
286
+ * Custom timeout for this operation in milliseconds. Overrides
287
+ * `KVServiceConfig.timeout`; `0` disables a configured timeout.
288
+ */
289
+ timeout?: number;
290
+ /**
291
+ * Custom abort signal for this operation.
292
+ */
293
+ signal?: AbortSignal;
294
+ }
295
+ /**
296
+ * Response headers from KV operations.
297
+ */
298
+ interface KVResponseHeaders {
299
+ /**
300
+ * ETag for conditional requests.
301
+ */
302
+ etag?: string;
303
+ /**
304
+ * Content type of the stored value.
305
+ */
306
+ contentType?: string;
307
+ /**
308
+ * Last modification timestamp.
309
+ */
310
+ lastModified?: string;
311
+ /**
312
+ * Content length in bytes.
313
+ */
314
+ contentLength?: number;
315
+ /**
316
+ * Get a header value by name.
317
+ * @param name - Header name (case-insensitive)
318
+ */
319
+ get(name: string): string | null;
320
+ }
321
+ /**
322
+ * Response from KV get/put operations.
323
+ *
324
+ * @template T - Type of the data payload
325
+ */
326
+ interface KVResponse<T = unknown> {
327
+ /**
328
+ * The data payload.
329
+ * For get: the stored value.
330
+ * For put: undefined.
331
+ */
332
+ data: T;
333
+ /**
334
+ * Response headers with metadata.
335
+ */
336
+ headers: KVResponseHeaders;
337
+ }
338
+ /**
339
+ * Response from KV list operations.
340
+ */
341
+ interface KVListResponse {
342
+ /**
343
+ * Array of keys matching the list criteria.
344
+ */
345
+ keys: string[];
346
+ /** True when more matching keys exist than the requested limit. */
347
+ truncated?: boolean;
348
+ /** Opaque cursor for the next page, when the node returned one. */
349
+ nextCursor?: string;
350
+ }
351
+ /**
352
+ * Options for {@link IKVService.changes}, the `tinycloud.kv/sync` change feed.
353
+ */
354
+ interface KVChangesOptions {
355
+ /**
356
+ * The space-relative KV prefix to follow, sent as the invocation path. It
357
+ * is not joined with the service's configured prefix, and must be non-empty.
358
+ * Prefixes match whole path segments: `notes` covers `notes` and
359
+ * `notes/a`, while `notes/` covers everything under `notes/` but not
360
+ * `notes` itself. The session must hold `tinycloud.kv/sync` on it by name.
361
+ *
362
+ * Returned `changes[].key` values are full paths in the space, like this
363
+ * prefix. On a service with a configured prefix (for example
364
+ * `DelegatedAccess.kv`, whose prefix is the delegation path), `get` joins
365
+ * that prefix again, so strip it from the key first, or follow the feed
366
+ * with `kv.withPrefix(...).changes()`, which returns relative keys.
367
+ */
368
+ prefix: string;
369
+ /**
370
+ * The `cursor` from the previous page. Omit it to bootstrap from the start
371
+ * of the prefix. Opaque; never parse or build one.
372
+ */
373
+ cursor?: string;
374
+ /**
375
+ * Page size, 1 through 1000 (node default 500). A page never splits one
376
+ * invocation's changes, so it can hold more than `limit` changes.
377
+ */
378
+ limit?: number;
379
+ /**
380
+ * CID of a never-invoked delegation carrying `tinycloud.kv/retain` on the
381
+ * prefix. When valid, the node attests `authority.retainUntil`.
382
+ */
383
+ retentionGrant?: string;
384
+ /**
385
+ * Custom timeout for this operation in milliseconds. Overrides
386
+ * `KVServiceConfig.timeout`; `0` disables a configured timeout.
387
+ */
388
+ timeout?: number;
389
+ /**
390
+ * Custom abort signal for this operation.
391
+ */
392
+ signal?: AbortSignal;
393
+ }
394
+ /**
395
+ * One key's latest state in a change feed page. Content is not included:
396
+ * read it with `get`/`batchGet` and check it against `etag`.
397
+ */
398
+ type KVChange = {
399
+ key: string;
400
+ deleted: false;
401
+ /** The strong ETag `get` returns for this state. */
402
+ etag: string;
403
+ metadata: Record<string, string>;
404
+ } | {
405
+ key: string;
406
+ deleted: true;
407
+ };
408
+ /**
409
+ * The node-attested window in which the caller's authority holds. Values are
410
+ * RFC 3339 timestamps, or `null` when nothing in the chain sets one.
411
+ */
412
+ interface KVChangesAuthority {
413
+ /** The latest `nbf` across the invocation's whole delegation chain. */
414
+ notBefore: string | null;
415
+ /** The earliest `exp` across the invocation's whole delegation chain. */
416
+ expiresAt: string | null;
417
+ /**
418
+ * The earliest `exp` across a valid retention grant's own chain. `null`
419
+ * means no retention: none was presented, or its chain sets no expiry.
420
+ */
421
+ retainUntil: string | null;
422
+ }
423
+ /**
424
+ * One page of the `tinycloud.kv/sync` change feed.
425
+ */
426
+ interface KVChangesResponse {
427
+ /** Each changed key's latest state, in commit order. */
428
+ changes: KVChange[];
429
+ /**
430
+ * More changes are available now. A page may be empty with `more: true`;
431
+ * keep paging with the returned cursor.
432
+ */
433
+ more: boolean;
434
+ /**
435
+ * The cursor for the next request. An empty poll returns the request's
436
+ * cursor unchanged.
437
+ */
438
+ cursor: string;
439
+ /** The node, space and prefix that served the page. */
440
+ source: {
441
+ nodeDid: string;
442
+ space: string;
443
+ prefix: string;
444
+ };
445
+ authority: KVChangesAuthority;
446
+ }
447
+ /**
448
+ * Response from signed KV read URL creation.
449
+ */
450
+ interface KVSignedReadUrlResponse {
451
+ /**
452
+ * Absolute URL suitable for passing to external readers.
453
+ */
454
+ url: string;
455
+ /**
456
+ * Opaque URL returned by tinycloud-node, usually relative to the node host.
457
+ */
458
+ relativeUrl: string;
459
+ /**
460
+ * Opaque signed KV ticket identifier.
461
+ */
462
+ ticketId: string;
463
+ /**
464
+ * Expiry timestamp as returned by tinycloud-node.
465
+ */
466
+ expiresAt: string;
467
+ }
468
+ /**
469
+ * KV service action types.
470
+ */
471
+ declare const KVAction: {
472
+ readonly GET: "tinycloud.kv/get";
473
+ readonly PUT: "tinycloud.kv/put";
474
+ readonly LIST: "tinycloud.kv/list";
475
+ readonly DELETE: "tinycloud.kv/del";
476
+ readonly HEAD: "tinycloud.kv/metadata";
477
+ /** The change feed; never implied by `*` or `tinycloud.kv/*`. */
478
+ readonly SYNC: "tinycloud.kv/sync";
479
+ /** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
480
+ readonly RETAIN: "tinycloud.kv/retain";
481
+ };
482
+ type KVActionType = (typeof KVAction)[keyof typeof KVAction];
483
+
484
+ /**
485
+ * Lifecycle / epoch gate contract (TC-858 Phase 2, CONTRACT.md §2). Types only.
486
+ *
487
+ * One interface set, implemented per platform. The browser keeps the gate in the origin
488
+ * database's control stores plus the `localStorage` logout marker and deny tombstone; node/CLI
489
+ * keep it in a gate file next to the replicas (`<profile>/replica-gate.json`). `Tx` is the
490
+ * guarded operation's platform context: `IDBTransaction` in the browser; SQLite transaction
491
+ * context for node replica pages/reads, or file context for node identity-scoped pending JSON
492
+ * under the mandatory profile lock.
493
+ *
494
+ * Construction boundary (browser): a `ReplicaGateTx` is constructed alongside whichever context
495
+ * owns the transaction — the worker for replica open/read/page/repair/GC/erase (with
496
+ * `ReplicaPartitionEraser` and `ReplicaQuotaLedger`, via `createKvWorkerHost({ gateTx, ledgerFor
497
+ * })`), and the main thread for the pending store (its `update(mutate)` closure runs there). Both
498
+ * instances open the same origin database. No `IDBTransaction` and no closure ever crosses
499
+ * postMessage. `ReplicaLifecycleGate` is the main-thread facade.
500
+ */
501
+
502
+ /** Injected millisecond clock shared by controller, browser lifecycle and worker; defaults to Date.now. */
503
+ type ReplicaClock = () => number;
504
+ /**
505
+ * Bound to one session object for its whole life. Created only by `establishSessionStamp` on a
506
+ * fresh authenticated session with a verified finite consent anchor, persisted in sdk-core
507
+ * `PersistedSessionData.replicaStamp`, and validated, never re-established, on restore. A session
508
+ * without a stamp is "not established" and is refused.
509
+ */
510
+ interface ReplicaSessionStamp {
511
+ /** Random id generated when the gate store is created; changes when it is deleted and recreated. */
512
+ readonly incarnation: string;
513
+ /** Non-negative integer; +1 on every deny; never decreases within an incarnation. */
514
+ readonly lifecycleEpoch: number;
515
+ }
516
+ /**
517
+ * One durable account gate per origin or node/CLI profile. No plaintext principal is stored:
518
+ * browser `principalHash` uses a private nonextractable gate-HMAC key with domain separation;
519
+ * node derives its salted hash before entering a guarded transaction.
520
+ */
521
+ interface ReplicaAccountGate {
522
+ incarnation: string;
523
+ /** Node: random base64url salt (≥ 32 bytes). Browser may carry one but MUST use its private
524
+ * gate-HMAC key, never this public value, for replica-owned identifiers. */
525
+ salt: string;
526
+ /** Active account's principal hash; null after sign-out. */
527
+ principalHash: string | null;
528
+ lifecycleEpoch: number;
529
+ /** ms since epoch of the last deny (browser: preserved across database recreation). */
530
+ deniedAt: number | null;
531
+ /** Browser: finite ms, fixed from verified fresh consent at establish; mandatory while active. */
532
+ consentDeadline?: number | null;
533
+ }
534
+ /**
535
+ * Node/CLI lowercase-hex HMAC-SHA-256, key = base64url-decoded salt, material = UTF-8.
536
+ * Browser identifiers MUST NOT use this public-salt helper.
537
+ */
538
+ type SaltedHash = (salt: string, material: string) => Promise<string>;
539
+ /**
540
+ * Browser lowercase-hex HMAC-SHA-256 using a nonextractable gate key over UTF-8
541
+ * `${domain}\0${material}`. Derive outside an IDB transaction; domains separate principals,
542
+ * identities, partitions and owner lookups. The private key is deleted on deny/expiry.
543
+ */
544
+ type ReplicaPrivateGateHash = (key: CryptoKey, domain: "principal" | "identity" | "partition" | "lookup", material: string) => Promise<string>;
545
+ /** Current gate incarnation, node salt and epoch; browser specialization also carries private key and ids. */
546
+ interface ReplicaGateSnapshot {
547
+ incarnation: string;
548
+ /** Node hashes with this value; browser never uses it for account identifiers. */
549
+ salt: string;
550
+ lifecycleEpoch: number;
551
+ }
552
+ /** Browser snapshot binds the nonextractable key and both key records to one gate generation. */
553
+ interface ReplicaBrowserGateSnapshot extends ReplicaGateSnapshot {
554
+ keyId: string;
555
+ accountKeyId: string;
556
+ gateHmac: CryptoKey;
557
+ }
558
+ /**
559
+ * Precomputed, stored opaque ids presented to one guarded operation. Derive principalHash
560
+ * before `run` (browser: private gate-HMAC from snapshot; node: salted hash before the
561
+ * profile-locked check). No raw principal or async WebCrypto computation inside the browser
562
+ * IDB transaction. `run` rechecks the snapshot's incarnation, stamp and stored principal hash.
563
+ */
564
+ interface ReplicaGateAccess {
565
+ /** Required: no unstamped access when a gate exists. */
566
+ stamp: ReplicaSessionStamp;
567
+ incarnation: string;
568
+ principalHash: string;
569
+ /** Stored partition id; refused while a purge intent covers it. */
570
+ partitionId?: string;
571
+ /** Stored identity id; refused while a pending intent covers it. */
572
+ identityKeyHash?: string;
573
+ }
574
+ /** Browser guard requires both key ids; missing ids cannot silently compile or enter `run`. */
575
+ interface ReplicaBrowserGateAccess extends ReplicaGateAccess {
576
+ keyId: string;
577
+ accountKeyId: string;
578
+ }
579
+ /** Why the gate refused (`REPLICA_GATED` errors carry this as `gate`). */
580
+ type ReplicaGateReason =
581
+ /** No stamp: the session was never established, or a restore had no persisted stamp. */
582
+ "not_established"
583
+ /** Incarnation or lifecycleEpoch differs from the gate (signed out, switched, or DB recreated). */
584
+ | "stamp_stale"
585
+ /** The gate's principal is null or another account. */
586
+ | "principal_mismatch"
587
+ /** A purge intent covers the partition/identity, or activation awaits the previous principal's erase. */
588
+ | "purge_pending"
589
+ /** Browser only: the `localStorage` logout marker is set. */
590
+ | "logout_pending"
591
+ /** The gate store itself could not be read; fail closed. */
592
+ | "gate_unavailable";
593
+ /** Shape of every gate refusal error: `Error & { code: "REPLICA_GATED"; gate }`. */
594
+ interface ReplicaGatedError extends Error {
595
+ code: "REPLICA_GATED";
596
+ gate: ReplicaGateReason;
597
+ }
598
+ /** Main-thread synchronous preflight: the controller observes expiry even without a worker read. */
599
+ type ReplicaDeliveryDecision = {
600
+ status: "allow";
601
+ } | {
602
+ status: "consent_expired";
603
+ code: "CONSENT_EXPIRED";
604
+ } | {
605
+ status: "gated";
606
+ gate: ReplicaGateReason;
607
+ };
608
+ /** Store names a guarded transaction may touch (data stores, plus `device` for owner-device and anchor reads). */
609
+ type ReplicaGuardedStore = (typeof REPLICA_KV_STORES)["meta" | "entries" | "blobs" | "pending" | "device"];
610
+ /** Store set and mode of one guarded transaction. Control stores are always added by `run`. */
611
+ interface ReplicaGateScope {
612
+ stores: readonly ReplicaGuardedStore[];
613
+ mode: "readonly" | "readwrite";
614
+ }
615
+ /**
616
+ * Every partition ever created in the gate's scope. Browser opaque IDs are private
617
+ * gate-HMAC outputs, never plaintext principal, prefix or device. `lookupId` is independent
618
+ * of sealed owner device DID and indexes one or more stored partition ids for ungated
619
+ * inspection/purge while the gate key exists; after deny, erasure uses recorded intents.
620
+ * The key in IDB is NOT protected from a raw browser-profile reader with a valid session.
621
+ * Node identity dirs retain their known unsalted base32 exposure (CONTRACT.md §1).
622
+ */
623
+ interface ReplicaPartitionIndexEntry {
624
+ partitionId: string;
625
+ principalHash: string;
626
+ identityKeyHash: string;
627
+ /** `gateHash(key, "lookup", identityKey + "\n" + prefix)`; nonunique across devices. */
628
+ lookupId: string;
629
+ /** True only when the adapter minted/used its stored owner device (`spec.device` absent). */
630
+ ownerDevice: boolean;
631
+ /** ms since epoch. */
632
+ createdAt: number;
633
+ }
634
+ /** A partition (or an identity's pending-write record) that must be erased before anything reads it. */
635
+ type ReplicaPurgeIntent = {
636
+ id: string;
637
+ kind: "partition";
638
+ partitionId: string;
639
+ principalHash: string;
640
+ recordedAt: number;
641
+ } | {
642
+ id: string;
643
+ kind: "pending";
644
+ identityKeyHash: string;
645
+ principalHash: string;
646
+ recordedAt: number;
647
+ };
648
+ interface ReplicaDenyInput {
649
+ /** Every scoped denial clears the current gate principal, including account switch. */
650
+ reason: "sign_out" | "account_switch" | "consent_expired";
651
+ /**
652
+ * The caller's own principal. If it doesn't match the gate, this stale caller disables only
653
+ * itself in memory, leaving the gate and successor account untouched.
654
+ */
655
+ principal: string;
656
+ /** Intended successor on switch, never stored under the old account/gate HMAC key. */
657
+ next?: string;
658
+ }
659
+ /**
660
+ * Result of step 1 (deny + intent). Never rejects: sign-out always succeeds in memory. Every
661
+ * outcome has already flipped this tab's/process's in-memory view (so `deliverable` is false)
662
+ * BEFORE `deny`'s first await.
663
+ */
664
+ type ReplicaDenyOutcome =
665
+ /**
666
+ * Gate moved, intents recorded, first-install anchors cleared and pending generations advanced
667
+ * in one commit. Browser: owner device deleted in that same IDB transaction. Node: the gate
668
+ * file cannot commit atomically with the device file, so the device is durably deleted by its
669
+ * identity's pending-intent erase before that intent clears (idempotent replay).
670
+ */
671
+ {
672
+ status: "denied";
673
+ lifecycleEpoch: number;
674
+ intents: number;
675
+ }
676
+ /** The caller's principal didn't match the gate: disabled in memory only; gate untouched. */
677
+ | {
678
+ status: "stale";
679
+ }
680
+ /** Browser: deny commit failed; `deleteDatabase` succeeded, so nothing of any account remains. */
681
+ | {
682
+ status: "deleted";
683
+ }
684
+ /** Browser: deny and delete failed; logout marker written, delete retried on every load. */
685
+ | {
686
+ status: "marker";
687
+ code: string;
688
+ }
689
+ /** Nothing could be written. Cross-tab denial NOT guaranteed; this tab serves nothing. */
690
+ | {
691
+ status: "unguarded";
692
+ code: string;
693
+ };
694
+ interface ReplicaEraseReport {
695
+ erased: string[];
696
+ failed: Array<{
697
+ id: string;
698
+ code: string;
699
+ }>;
700
+ /** Intents still outstanding after this pass. */
701
+ remaining: number;
702
+ /** Browser startup discovered elapsed consent and durably crypto-shredded the account/gate keys. */
703
+ consentExpired?: boolean;
704
+ }
705
+ /**
706
+ * The structural guard (browser: one instance per transaction-owning context). Every
707
+ * session-bound transaction reading or writing `meta`, `entries`, `blobs`, `pending` or
708
+ * `device` MUST go through `run`: open, get/list, status, `grant()`, installGrant,
709
+ * page/repair/GC/sync completion and pending reads/updates. Narrow exceptions:
710
+ * purge/erase, pre-transaction key/control `snapshot()` and ungated bounded read-only
711
+ * `inspectStatus` (§1), which rechecks the gate key and reads only opaque numeric controls,
712
+ * never acquiring the account key, decrypting payloads or returning values/list keys.
713
+ */
714
+ interface ReplicaGateTx<Tx, Access extends ReplicaGateAccess = ReplicaGateAccess, Snapshot extends ReplicaGateSnapshot = ReplicaGateSnapshot> {
715
+ /**
716
+ * Opens the guarded operation. Browser: spans `scope.stores` plus all control stores
717
+ * (`gate`, `partitions`, `purgeIntents`, `quota`, `device`). The FIRST IDB request reads
718
+ * the gate; following control requests verify both stored nonextractable key records
719
+ * and intents **before any data request**. Compare snapshot incarnation/keyIds, stamp,
720
+ * principalHash and consent deadline; mismatch aborts before `fn` or writes. No WebCrypto
721
+ * await occurs inside the transaction. On active browser expiry, abort/read completion
722
+ * precedes an awaited main-thread deny/key-shred RPC; failed denial fails closed.
723
+ * `fn` awaits IDB requests only; readwrite uses strict durability and `run` resolves
724
+ * on IDB `oncomplete`.
725
+ *
726
+ * Node SQLite mutations hold the mandatory profile lock across rechecked gate and commit;
727
+ * reads check the gate and partition tombstone inside a SQLite read transaction. Node
728
+ * identity-scoped `pending.json` is not SQLite: reads/writes hold the profile lock across
729
+ * gate, intent, generation and file access (CONTRACT.md §2). Refusal rejects with
730
+ * {@link ReplicaGatedError}; an active browser deadline reports CONSENT_EXPIRED first.
731
+ */
732
+ run<T>(scope: ReplicaGateScope, access: Access, fn: (tx: Tx) => Promise<T>): Promise<T>;
733
+ /**
734
+ * Snapshot outside any guarded data transaction. Browser callers obtain the
735
+ * nonextractable gate-HMAC key and key ids, derive private hashes asynchronously,
736
+ * take the opaque Web Lock if needed, then `run` rechecks key ids/stamp before data.
737
+ * This control/key read never acquires the account key for ungated inspection.
738
+ * Node callers derive their salted principal hash before guarded work.
739
+ */
740
+ snapshot(): Promise<Snapshot | null>;
741
+ /** Registers a partition in the index, inside the `run` transaction that creates it. Idempotent. */
742
+ indexPartition(tx: Tx, entry: ReplicaPartitionIndexEntry): Promise<void>;
743
+ /** Removes the partition's index entry, inside its erase transaction. Idempotent. */
744
+ unindexPartition(tx: Tx, partitionId: string): Promise<void>;
745
+ /** Removes the intent `id`, in the same transaction as the erase it records (node: after it, see CONTRACT.md §2). */
746
+ clearIntent(tx: Tx, id: string): Promise<void>;
747
+ }
748
+ /** Browser guard: typed private snapshot and required prehashed/key-bound access. */
749
+ type ReplicaBrowserGateTx<Tx> = ReplicaGateTx<Tx, ReplicaBrowserGateAccess, ReplicaBrowserGateSnapshot>;
750
+ /**
751
+ * Erase seam implemented by the storage adapter, driven by the gate (worker-side in the browser).
752
+ * Browser erase and ledger accounting share an exempt IDB transaction with index/intent removal.
753
+ * Node partition erase uses SQLite tombstone/row deletion before unlink and intent clear; node
754
+ * pending identity erase runs under the profile lock over `pending.json` and `device.jwk`, not a
755
+ * SQLite transaction. All replay is idempotent.
756
+ */
757
+ interface ReplicaPartitionEraser<Tx> {
758
+ /**
759
+ * Browser: writes a keyless `purged: true` meta tombstone with only opaque pid/numeric
760
+ * controls (so open handles fail RESET_REQUIRED by generation), then removes entries,
761
+ * blobs, grant, config and authority. `bytes` is the old application-encoded rows
762
+ * minus the tombstone; its measured size stays charged until replacement/deletion,
763
+ * and replay returns zero without releasing it twice. Node: a SQLite tombstone
764
+ * fences open handles; its erasure has no browser quota-ledger semantics.
765
+ */
766
+ erasePartition(tx: Tx, partitionId: string): Promise<{
767
+ bytes: number;
768
+ }>;
769
+ /** Removes identity pending state (node: also `device.jwk`) durably before intent clear. */
770
+ eraseIdentityPending(tx: Tx, identityKeyHash: string): Promise<void>;
771
+ }
772
+ /**
773
+ * Main-thread facade over the platform gate, exposed as `KVReplicaStorage.lifecycle`. With
774
+ * replication on, storage MUST expose it (browser: the web runtime refuses storage without it with
775
+ * RUNTIME_UNSUPPORTED; node: `sqliteReplicaStorage` exposes it by default).
776
+ */
777
+ interface ReplicaLifecycleGate {
778
+ readonly kind: "indexeddb" | "file";
779
+ /**
780
+ * Startup recovery before anything binds: browser checks the durable consent deadline and,
781
+ * if expired, denies and crypto-shreds keys before any replica access; it also retries a
782
+ * pending `deleteDatabase` marker and erases intents. Failures remain gated and visible via
783
+ * `pendingPurge()` / `purgePending`; a browser closed at expiry catches it on its next load.
784
+ */
785
+ recover(o: {
786
+ signal: AbortSignal;
787
+ }): Promise<ReplicaEraseReport>;
788
+ /**
789
+ * Fresh interactive sign-in ONLY. `consentAnchor` comes from independently validated
790
+ * signed owner SIWE with finite `expirationTime`, verified `issuedAt` and an exact-full-
791
+ * space uncaveated KV get+sync witness, or (for a compact-only session) the signed
792
+ * `iat`/`nbf` of the oldest independently verified owner-authorizing proof. A child
793
+ * leaf freshly minted under an old owner grant does not refresh consent; leaf
794
+ * signature/CID/audience alone do not prove it. An invalid/expirationless SIWE
795
+ * session does NOT fall through to a compact leaf or `first_install`. The anchor
796
+ * must be finite ms, clamped to now at capture and strictly newer than the last
797
+ * deny (browser: also the origin tombstone). Missing proof or non-finite anchors
798
+ * cannot establish (`not_established`); network sign-in/restore remain unaffected.
799
+ * Older anchors reject `stamp_stale`. Rejects
800
+ * `logout_pending` / `purge_pending` before mutation. Null principal → set it; matching
801
+ * principal → current stamp; another account → `principal_mismatch`.
802
+ */
803
+ establishSessionStamp(principal: string, o: {
804
+ consentAnchor: ReplicaConsentAnchor;
805
+ }): Promise<ReplicaSessionStamp>;
806
+ /**
807
+ * Restore ONLY; never mutates the gate. Returns `persisted` when it equals the gate's stamp and
808
+ * principal. Rejects `not_established` (no persisted stamp), `stamp_stale`, `principal_mismatch`,
809
+ * `logout_pending`, `purge_pending`.
810
+ */
811
+ restoreSessionStamp(principal: string, persisted: ReplicaSessionStamp | undefined): Promise<ReplicaSessionStamp>;
812
+ /** True only for a matching non-null gate principal, with no old intent or marker. A null principal first establishes a fresh session. */
813
+ activationAllowed(next: string): Promise<boolean>;
814
+ /**
815
+ * Synchronous, reason-bearing preflight before open, installGrant, get/list, sync or
816
+ * replica-derived event delivery. An elapsed browser deadline returns `consent_expired`
817
+ * before grant-expiry checks; the controller starts scoped denial and emits one grant
818
+ * deadline event per **opened** handle with a known opaque replicaId. Pre-open denial has
819
+ * no fabricated replica id and uses a lifecycle event/recovery report instead. A timer
820
+ * that already denied returns `gated` (`stamp_stale`). This does not mutate the gate.
821
+ */
822
+ checkDelivery(stamp: ReplicaSessionStamp): ReplicaDeliveryDecision;
823
+ /**
824
+ * Finite, immutable deadline from the persisted gate for this matching active stamp,
825
+ * synchronously available to the main-thread scheduler even when a restored compact
826
+ * session has no consent anchor. Null if stale, unavailable or not established; never
827
+ * derive a new deadline from `first_install` or the current session's leaf token.
828
+ * Browser timers call `checkDelivery` again before notifying/denying; gate checks on
829
+ * startup/open/read are mandatory independently of whether a timer ran. Node may
830
+ * return null when its gate has no persisted consent deadline.
831
+ */
832
+ consentDeadline(stamp: ReplicaSessionStamp): number | null;
833
+ /**
834
+ * Synchronous main-thread delivery check before replica-served values/list keys/event fields,
835
+ * and before open, installGrant and sync. Browser checks marker, origin deny tombstone,
836
+ * durable consent deadline against the same injected clock, and this tab's gate view; node
837
+ * checks its process view. No stamp means not deliverable.
838
+ */
839
+ deliverable(stamp: ReplicaSessionStamp): boolean;
840
+ /**
841
+ * Step 1. Flips the in-memory view before the first await. If the caller principal matches,
842
+ * one commit bumps epoch and deniedAt, records intents, advances pending generations and
843
+ * clears first-install anchors. Browser also deletes account AES-GCM and gate-HMAC keys, plus
844
+ * owner device, in that deny transaction BEFORE background erase; `consent_expired` uses this
845
+ * same path. Node deletes the device through the identity intent (CONTRACT.md §2). A stale
846
+ * principal disables only its own process/tab; the gate is untouched.
847
+ */
848
+ deny(input: ReplicaDenyInput): Promise<ReplicaDenyOutcome>;
849
+ /** Step 3: erase each outstanding intent in its own exempt transaction. */
850
+ erasePending(o: {
851
+ signal: AbortSignal;
852
+ }): Promise<ReplicaEraseReport>;
853
+ /** Outstanding intents; non-empty means status `purgePending: true`. */
854
+ pendingPurge(): Promise<ReplicaPurgeIntent[]>;
855
+ }
856
+ /** Worker replies that carry replica-served data are stamped; a reply without a stamp is dropped. */
857
+ interface ReplicaStampedReply {
858
+ stamp: ReplicaSessionStamp;
859
+ }
860
+
861
+ /**
862
+ * Quota contract (TC-858 Phase 2, CONTRACT.md §4). Types only.
863
+ *
864
+ * Browser: `maxBytes` is origin-wide over the deterministic application encoding of
865
+ * ciphertext entries/blobs/meta and sealed owner-device plus grant/first-install device
866
+ * records. The ledger lives in the `quota` control store; page and device writes admit
867
+ * before any put, then account in the same guarded transaction. Deny releases the reserved
868
+ * device bucket atomically with key deletion; partition erase remains idempotent.
869
+ * Node/CLI: `maxBytes` is per profile (per storage `dir`); there is no ledger — usage is derived
870
+ * from stored content, so erase replay is safe — and the `admit` hook runs inside `applyPage`'s
871
+ * guarded mutation (profile lock held). `minFreeBytes` applies only in the browser.
872
+ */
873
+ /** Quota settings forwarded from `ReplicationOptions` to storage through `KVReplicaSpec.quota`. */
874
+ interface ReplicaQuotaSpec {
875
+ /** Byte budget; absent means unbounded (Phase 1 behaviour). */
876
+ maxBytes?: number;
877
+ /** Browser free-space floor; the browser adapter defaults it to `DEFAULT_MIN_FREE_BYTES`. */
878
+ minFreeBytes?: number;
879
+ }
880
+ /** Node/CLI page admission keeps its existing blob/entry interpretation. */
881
+ interface ReplicaPageAdmission {
882
+ partitionId: string;
883
+ /** Sum of the page's new blob bytes (node admission hook's existing metric). */
884
+ blobBytes: number;
885
+ /** Number of entry writes; not the browser ciphertext-row accounting metric. */
886
+ entries: number;
887
+ /** Optional caller-supplied free bytes; node's browser-only floor is not applied. */
888
+ freeBytes: number | null;
889
+ }
890
+ /**
891
+ * Browser page admission uses the exact signed net application-encoded byte change of
892
+ * persisted ciphertext entries, blobs and active meta. Erase retains a keyless opaque/numeric
893
+ * purged meta tombstone, charged to the partition until replacement or physical deletion;
894
+ * the same row metric measures it. S5's browser ledger separately combines measured device-row
895
+ * deltas for owner/stamp writes in a reserved device bucket. New rows are sealed before opening
896
+ * the transaction; old stored rows are measured inside it. Replacements/GC may release space.
897
+ * Node's page admission stays unchanged.
898
+ */
899
+ interface ReplicaBrowserPageAdmission {
900
+ partitionId: string;
901
+ deltaBytes: number;
902
+ /** Free bytes read before opening the transaction; null when unknown. */
903
+ freeBytes: number | null;
904
+ }
905
+ /**
906
+ * Browser ledger seam, called by the adapter in guarded write or exempt erase/deny
907
+ * transactions that include the `quota` store. Every sealed meta mutation (including
908
+ * lease/status) and measured device write uses the origin ledger. Implementations MUST
909
+ * only await requests on `tx`; S5's browser implementation adds combined-row methods
910
+ * without changing this shared public interface.
911
+ */
912
+ interface ReplicaQuotaLedger<Tx> {
913
+ /** Read outside any transaction, just before it: `navigator.storage.estimate()` quota − usage. */
914
+ freeBytes(): Promise<number | null>;
915
+ /**
916
+ * Admits the exact projected application-encoded persisted ciphertext-row delta, including
917
+ * sealed value/IV/tag plus persisted key/index fields and envelope framing. IDB engine/page
918
+ * overhead is unobservable and excluded. The adapter uses the same measured delta for
919
+ * `accountCommit`; never admits plaintext lengths or a per-entry estimate. On
920
+ * STORAGE_BUDGET it aborts `tx` before any page write.
921
+ */
922
+ admitPage(tx: Tx, page: ReplicaBrowserPageAdmission): Promise<void>;
923
+ /** Records the same measured signed delta in the page/repair/GC/meta transaction. */
924
+ accountCommit(tx: Tx, o: {
925
+ partitionId: string;
926
+ deltaBytes: number;
927
+ }): Promise<void>;
928
+ /** Subtracts the exact encoded bytes removed in the erase transaction, idempotently. */
929
+ accountErase(tx: Tx, o: {
930
+ partitionId: string;
931
+ bytes: number;
932
+ }): Promise<void>;
933
+ }
934
+ /**
935
+ * Node/CLI admission hook (`admit` on the SQLite store options), run inside `applyPage`'s guarded
936
+ * mutation before any write. Rejects with code STORAGE_BUDGET to refuse the page.
937
+ */
938
+ type ReplicaAdmitHook = (page: ReplicaPageAdmission) => void | Promise<void>;
939
+
940
+ interface ReplicationIdentity {
941
+ /** Canonical HTTP(S) URL: origin + path without trailing slash; no credentials/query/fragment. */
942
+ host: string;
943
+ /** Full TinyCloud space id; only the EIP-155 address segment is lower-cased. */
944
+ space: string;
945
+ /** Principal `did:pkh:eip155:…`; only the EIP-155 address segment is lower-cased. */
946
+ principal: string;
947
+ }
948
+ type ReplicaCoverage = "empty" | "bootstrapping" | "complete";
949
+ type ReplicaAuthorityState = "valid" | "expired" | "revoked" | "not-yet-valid";
950
+ interface LocalReadMeta {
951
+ asOf: string | null;
952
+ coverage: ReplicaCoverage;
953
+ authority: ReplicaAuthorityState;
954
+ /** Durable identity commit epoch captured before the last successful sync began. */
955
+ syncedThroughEpoch: number;
956
+ }
957
+ type LocalGetResult = {
958
+ status: "present";
959
+ key: string;
960
+ value: Uint8Array;
961
+ etag: string;
962
+ metadata: Record<string, string>;
963
+ meta: LocalReadMeta;
964
+ } | {
965
+ status: "content_missing";
966
+ key: string;
967
+ etag: string;
968
+ metadata: Record<string, string>;
969
+ meta: LocalReadMeta;
970
+ } | {
971
+ status: "deleted" | "absent" | "coverage_incomplete" | "not_covered";
972
+ key: string;
973
+ meta: LocalReadMeta;
974
+ };
975
+ /** Live keys under a plain `startsWith` prefix, after-exclusive UTF-8 byte ordering, at most limit. */
976
+ interface LocalListResult {
977
+ keys: string[];
978
+ meta: LocalReadMeta;
979
+ }
980
+ type LocalSyncResult = {
981
+ status: "synced";
982
+ pages: number;
983
+ changes: number;
984
+ deleted: number;
985
+ fetched: number;
986
+ contentMissing: number;
987
+ coverage: ReplicaCoverage;
988
+ /** The input syncStartEpoch, persisted atomically only when this sync succeeds. */
989
+ syncedThroughEpoch: number;
990
+ } | {
991
+ status: "busy";
992
+ };
993
+ interface ReplicaGrantInfo {
994
+ cid: string;
995
+ parentCid: string | null;
996
+ /** The signed `exp`, ms since epoch; null when the grant has none. */
997
+ expiresAt: number | null;
998
+ /** Grant lifecycle: pending until confirmed/activated by a successful replica sync. */
999
+ state: "active" | "pending";
1000
+ /** Both get and sync in the grant carry no caveats. */
1001
+ unconstrained: boolean;
1002
+ /**
1003
+ * ms since epoch, from the storage clock at the FIRST install of this grant CID, stored keyed
1004
+ * by CID and set once. Re-installing the same CID never refreshes it; recompute-at-open uses the
1005
+ * stored value.
1006
+ */
1007
+ installedAt?: number;
1008
+ /**
1009
+ * min(expiresAt, installedAt + grantTtlMs, consentDeadline), stored with the grant and
1010
+ * recomputed at open from the stored `installedAt` and the handle's `spec.grantPolicy`. Serving
1011
+ * requires `now < effectiveExpiresAt` with a finite stored value. Otherwise (NaN, ±Infinity,
1012
+ * missing) the effective authority is not `valid`, and get/list throw GRANT_EXPIRED inside the
1013
+ * read transaction without returning bytes or keys. Present on every Phase 2 grant.
1014
+ */
1015
+ effectiveExpiresAt?: number;
1016
+ /**
1017
+ * ms since epoch; consent anchor + CONSENT_WINDOW_MS. Serving and minting require
1018
+ * `now < consentDeadline` and a finite value (refusal code CONSENT_EXPIRED).
1019
+ */
1020
+ consentDeadline?: number;
1021
+ /**
1022
+ * The resolved consent anchor (finite ms, clamped to ≤ now). A `first_install` anchor is keyed by
1023
+ * identity (node: gate file `firstInstall`; browser: `device` store), survives partition reset,
1024
+ * purge, revocation and reinstall, and is cleared only by `deny`. An unreadable anchor fails
1025
+ * closed (no serving, no mint).
1026
+ */
1027
+ consentAnchor?: ReplicaConsentAnchor;
1028
+ }
1029
+ interface LocalReplicaStatus {
1030
+ coverage: ReplicaCoverage;
1031
+ lastSyncAt: string | null;
1032
+ /** Durable identity commit epoch captured before the last successful sync began. */
1033
+ syncedThroughEpoch: number;
1034
+ authority: {
1035
+ state: ReplicaAuthorityState;
1036
+ expiresAt: string | null;
1037
+ };
1038
+ grant: ReplicaGrantInfo | null;
1039
+ counts: {
1040
+ keys: number;
1041
+ contentMissing: number;
1042
+ tombstones: number;
1043
+ };
1044
+ bytes: number;
1045
+ lastError: {
1046
+ at: string;
1047
+ code: string;
1048
+ message: string;
1049
+ } | null;
1050
+ /**
1051
+ * Browser: result of `navigator.storage.persist()`; null when the API is missing or not yet
1052
+ * asked. Node/CLI omit it (disk storage is persistent).
1053
+ */
1054
+ persisted?: boolean | null;
1055
+ /** A purge intent covers this partition (or its identity); it is unreadable until erased. */
1056
+ purgePending?: boolean;
1057
+ }
1058
+ /**
1059
+ * Ungated browser inspection returns only opaque, unsealed numeric controls from the
1060
+ * v2 meta row: coverageCode, syncedThroughEpoch, keys/contentMissing/tombstones and
1061
+ * contentBytes. It must not acquire the account key or decrypt grant, authority, cursor,
1062
+ * errors or KV payload. `redacted: true` distinguishes unknown from absent/revoked.
1063
+ */
1064
+ interface ReplicaInspectedStatus {
1065
+ redacted: true;
1066
+ coverage: ReplicaCoverage;
1067
+ syncedThroughEpoch: number;
1068
+ counts: LocalReplicaStatus["counts"];
1069
+ bytes: number;
1070
+ persisted?: boolean | null;
1071
+ purgePending?: boolean;
1072
+ }
1073
+ interface ReplicaDevice {
1074
+ did: string;
1075
+ jwk: object;
1076
+ }
1077
+ /** Identity is canonical; `space` is the session's verbatim space id for replica calls. */
1078
+ interface KVReplicaSpec {
1079
+ identity: ReplicationIdentity;
1080
+ space: string;
1081
+ prefix: string;
1082
+ allowSecrets: boolean;
1083
+ /**
1084
+ * When present, adapters MUST use this device for partition naming, grant audience checks,
1085
+ * and signing sync requests; they MUST NOT load or create their own device key for this handle.
1086
+ */
1087
+ device?: ReplicaDevice;
1088
+ /** Graph-scoped fetch for main-thread adapters; worker adapters ignore it. */
1089
+ fetch?: FetchFunction;
1090
+ /**
1091
+ * The session stamp this handle is bound to; immutable for the handle's life. With a `lifecycle`
1092
+ * gate (mandatory when replication is on), a spec without it is refused REPLICA_GATED
1093
+ * `not_established`, and every guarded transaction runs through `ReplicaGateTx.run` with it.
1094
+ * Use {@link ReplicaGatedSpec} where the stamp must be present.
1095
+ */
1096
+ stamp?: ReplicaSessionStamp;
1097
+ /**
1098
+ * Grant lifetime policy. Mandatory when replication is on: the controller always sets it, and a
1099
+ * Phase 2 adapter rejects `open` without it (REPLICA_INVALID_ARGUMENT). Optional in the type only
1100
+ * so Phase 1 callers compile.
1101
+ */
1102
+ grantPolicy?: ReplicaGrantPolicy;
1103
+ /** Quota settings; absent means unbounded. */
1104
+ quota?: ReplicaQuotaSpec;
1105
+ /**
1106
+ * Called when another tab/process commits a sync for this partition (browser: the worker's
1107
+ * `committed` BroadcastChannel message). The controller emits `replication.sync` with
1108
+ * `trigger: "peer"`, `origin: "peer"`. Must not throw; must not be called for own commits.
1109
+ */
1110
+ onPeerCommit?(info: ReplicaPeerCommit): void;
1111
+ }
1112
+ interface ReplicaPeerCommit {
1113
+ /** ms since epoch of the peer's commit. */
1114
+ at: number;
1115
+ syncedThroughEpoch: number;
1116
+ changes?: number;
1117
+ coverage?: ReplicaCoverage;
1118
+ }
1119
+ /** A spec as a gated adapter accepts it: stamp and grant policy present. */
1120
+ type ReplicaGatedSpec = KVReplicaSpec & {
1121
+ stamp: ReplicaSessionStamp;
1122
+ grantPolicy: ReplicaGrantPolicy;
1123
+ };
1124
+ interface KVReplicaHandle {
1125
+ readonly spec: KVReplicaSpec;
1126
+ /** Always equals `spec.device.did` when an explicit device was provided. */
1127
+ readonly deviceDid: string;
1128
+ /**
1129
+ * Opaque partition id for events. Required on Phase 2 `ReplicaGatedHandle`; optional here
1130
+ * only for Phase 1 adapters. Browser: private gate-HMAC partition id; node: replica hash.
1131
+ * A controller MUST NOT derive one from raw prefix/space or emit a grant event without it.
1132
+ */
1133
+ readonly replicaId?: string;
1134
+ /**
1135
+ * Guarded read. Adapters MUST NOT return value bytes unless the effective authority is `valid`
1136
+ * (signed exp, effective expiry, consent deadline, node-attested authority, revocation): they
1137
+ * throw GRANT_EXPIRED / GRANT_REVOKED / GRANT_NOT_YET_VALID / CONSENT_EXPIRED inside the read
1138
+ * transaction, and the controller falls through to the network with the matching reason. An
1139
+ * erased partition rejects RESET_REQUIRED (by tombstone/generation, not by unlink).
1140
+ */
1141
+ get(key: string): Promise<LocalGetResult>;
1142
+ /** Guarded read; same authority rule as `get` (no keys unless authority is `valid`). */
1143
+ list(o: {
1144
+ prefix: string;
1145
+ after?: string;
1146
+ limit?: number;
1147
+ }): Promise<LocalListResult>;
1148
+ /** Guarded read (runs through `ReplicaGateTx.run`). */
1149
+ grant(): Promise<ReplicaGrantInfo | null>;
1150
+ /**
1151
+ * Installs a grant after validating audience (the handle's device), `kv/get` + `kv/sync` coverage
1152
+ * of the prefix, and no caveats (caveated grants reject GRANT_NOT_COVERING). Stores `installedAt`
1153
+ * on first install of the CID and `effectiveExpiresAt`. Guarded: REPLICA_GATED.
1154
+ */
1155
+ installGrant(ucan: string): Promise<ReplicaGrantInfo>;
1156
+ /**
1157
+ * The controller supplies syncStartEpoch, captured from this identity's committedEpoch before
1158
+ * starting the sync. Every adapter, including the browser worker, MUST persist that exact epoch
1159
+ * in the same transaction as the sync's completion marker, only after the whole sync finishes
1160
+ * (feed pages, repairs and GC). Failed, aborted and busy syncs, including a failure after the
1161
+ * last feed page, MUST leave the prior fence unchanged. A successful result returns that
1162
+ * persisted value as syncedThroughEpoch. Aborting `signal` stops at the last complete page;
1163
+ * nothing past it commits. Gated: REPLICA_GATED; quota refusal: STORAGE_BUDGET / STORAGE_FULL,
1164
+ * keeping committed content.
1165
+ */
1166
+ sync(o: {
1167
+ signal: AbortSignal;
1168
+ syncStartEpoch: number;
1169
+ }): Promise<LocalSyncResult>;
1170
+ /** Status reports the durable syncedThroughEpoch, including after reopening this handle. */
1171
+ status(): Promise<LocalReplicaStatus>;
1172
+ /** Cancels and awaits in-flight syncs before closing. */
1173
+ close(): Promise<void>;
1174
+ }
1175
+ /** Phase 2 opened handle: immutable stamp/policy and an opaque event identifier. */
1176
+ type ReplicaGatedHandle = KVReplicaHandle & {
1177
+ readonly spec: ReplicaGatedSpec;
1178
+ readonly replicaId: string;
1179
+ };
1180
+ interface PurgeTarget {
1181
+ identity: ReplicationIdentity;
1182
+ /** Verbatim session space id used by the corresponding replica handle. */
1183
+ space: string;
1184
+ prefix: string;
1185
+ /** Session device in delegate posture, when one is used. */
1186
+ sessionDeviceDid?: string;
1187
+ }
1188
+ /**
1189
+ * Replica persistence adapters. B-node's Node/CLI adapter MUST provide durable pending state.
1190
+ * If that state cannot be opened (for example, a file-store failure), it MUST surface the
1191
+ * unavailable/error to the controller; the controller MUST fail closed to network reads and
1192
+ * MUST NOT silently substitute memory.
1193
+ */
1194
+ interface KVReplicaStorage {
1195
+ readonly kind: "sqlite" | "indexeddb";
1196
+ open(spec: KVReplicaSpec): Promise<KVReplicaHandle>;
1197
+ /**
1198
+ * Read an existing replica's durable status without creating/mutating storage or waiting for a
1199
+ * mutation guard. Missing replicas return undefined; abort is honored. Ungated (no stamp):
1200
+ * browser returns a discriminated, redacted control-only status without acquiring the account
1201
+ * key or decrypting data; node/CLI may expose existing grant CIDs/times. Known metadata exposure.
1202
+ */
1203
+ inspectStatus?(spec: KVReplicaSpec, options: {
1204
+ signal: AbortSignal;
1205
+ }): Promise<LocalReplicaStatus | ReplicaInspectedStatus | undefined>;
1206
+ /**
1207
+ * Erases replicas for the identity and prefix across the deduplicated union of the adapter's
1208
+ * stored device (if any) and `sessionDeviceDid`. A missing stored device never skips the
1209
+ * session-device purge. Does not open, mint, or sync. Erasure removes entries, blobs, grant,
1210
+ * config, and authority; earlier handles reject with RESET_REQUIRED. A contentless tombstone
1211
+ * may remain. Rejects with an error carrying `code` on failure.
1212
+ */
1213
+ purge(target: PurgeTarget): Promise<void>;
1214
+ /**
1215
+ * Optional only for volatile runtimes. Omission means an in-memory store with `durable: false`.
1216
+ * B-node's Node/CLI adapter MUST provide a durable store and surface open failures, never
1217
+ * silently fall back to memory.
1218
+ *
1219
+ * With a `lifecycle` gate, the store is bound to `options.stamp` for its whole life (callers
1220
+ * cache stores per identity AND stamp). Every `read`, `readExisting` and `update` (append,
1221
+ * settle, remove, clear, flush) checks stamp, principal, incarnation and identity intent
1222
+ * (plus browser keyId) before touching storage; missing stamps refuse REPLICA_GATED
1223
+ * `not_established`. Browser derives private gate-HMAC ids asynchronously from `snapshot()`
1224
+ * before an IDB transaction, locks on that opaque identity id, then calls main-thread
1225
+ * `ReplicaGateTx.run`, whose first control request verifies the snapshot/keyId. Node's
1226
+ * identity-scoped `pending.json` is not SQLite: both reads and mutations take the mandatory
1227
+ * profile lock across gate/generation check and file access; mutations durably rename
1228
+ * before releasing it.
1229
+ * The identity generation advances atomically with a deny intent, so stale stores cannot
1230
+ * refill erased pending writes (CONTRACT.md §2). Missing locks fail closed.
1231
+ */
1232
+ pendingWrites?(identity: ReplicationIdentity, options?: {
1233
+ stamp?: ReplicaSessionStamp;
1234
+ }): PendingWriteStore;
1235
+ /**
1236
+ * The platform lifecycle gate (browser: origin DB control stores + logout marker; node/CLI:
1237
+ * gate file). Mandatory when replication is on: `indexedDbReplicaStorage()` and
1238
+ * `sqliteReplicaStorage()` expose it (node's `gateFile` defaults next to `dir`), and the web
1239
+ * runtime refuses storage without it with RUNTIME_UNSUPPORTED. Optional in the type only so
1240
+ * Phase 1 test doubles compile; use {@link ReplicaGatedStorage} for Phase 2 adapters.
1241
+ */
1242
+ readonly lifecycle?: ReplicaLifecycleGate;
1243
+ }
1244
+ /** A Phase 2 adapter: the unstamped Phase 1 `open` overload is deliberately absent. */
1245
+ type ReplicaGatedStorage = Omit<KVReplicaStorage, "open" | "lifecycle"> & {
1246
+ readonly lifecycle: ReplicaLifecycleGate;
1247
+ open(spec: ReplicaGatedSpec): Promise<ReplicaGatedHandle>;
1248
+ };
1249
+ interface PendingWriteRecord {
1250
+ opId: string;
1251
+ seq: number;
1252
+ key: string;
1253
+ op: "put" | "delete";
1254
+ state: "in_flight" | "committed" | "ambiguous";
1255
+ epoch: number | null;
1256
+ at: string;
1257
+ settledAt: string | null;
1258
+ code?: string;
1259
+ }
1260
+ /** One identity-scoped pending state, with a monotonically increasing commit epoch and sequence. */
1261
+ interface PendingWriteState {
1262
+ v: 2;
1263
+ identity: ReplicationIdentity;
1264
+ committedEpoch: number;
1265
+ seq: number;
1266
+ records: PendingWriteRecord[];
1267
+ }
1268
+ /** Identity-scoped pending state and its restart-durability guarantee. */
1269
+ interface PendingWriteStore {
1270
+ /**
1271
+ * True only when committedEpoch and all records survive process restarts; false for memory or
1272
+ * other volatile stores.
1273
+ */
1274
+ readonly durable: boolean;
1275
+ readonly identity: ReplicationIdentity;
1276
+ /**
1277
+ * Consistent gated snapshot. Browser: guarded IDB read; node/CLI: mandatory profile lock
1278
+ * across gate check and pending.json read (not an unlocked atomic-file snapshot).
1279
+ */
1280
+ read(): Promise<PendingWriteState>;
1281
+ /**
1282
+ * Read an existing durable snapshot without initializing storage; undefined means no file exists.
1283
+ * Gated.
1284
+ */
1285
+ readExisting?(): Promise<PendingWriteState | undefined>;
1286
+ /**
1287
+ * Serialized read-modify-write; durable stores persist before resolve. Errors on mismatch/failure.
1288
+ * Gated: the assert runs inside the same transaction/guard as the write.
1289
+ */
1290
+ update<T>(mutate: (s: PendingWriteState) => T): Promise<T>;
1291
+ }
1292
+ type AuthorityRefusal = {
1293
+ refused: "NOT_COVERED" | "CAVEATED_AUTHORITY" | "SESSION_EXPIRING";
1294
+ };
1295
+ interface ReplicationAuthority {
1296
+ /**
1297
+ * Compact delegated/session-only authority: never consult `plan`, mint or silently renew.
1298
+ * A matching persisted stamp and independently verified, active installed grant are not
1299
+ * enough to serve locally: the CURRENT session must also have uncaveated get authority
1300
+ * for each requested key/list prefix. If `sessionGrant(prefix)` refuses NOT_COVERED,
1301
+ * CAVEATED_AUTHORITY or SESSION_EXPIRING, use network KV even while that stored grant
1302
+ * remains active. A stored grant never establishes fresh consent or replaces the
1303
+ * current session's own authority.
1304
+ */
1305
+ sessionOnly?: boolean;
1306
+ sessionGrant(prefix: string): {
1307
+ ucan: string;
1308
+ device: ReplicaDevice;
1309
+ } | AuthorityRefusal;
1310
+ plan(prefix: string): {
1311
+ path: "session" | "runtime";
1312
+ parentCid: string;
1313
+ expiresAt: number;
1314
+ } | AuthorityRefusal;
1315
+ mint(deviceDid: string, prefix: string, signal: AbortSignal,
1316
+ /**
1317
+ * `expiryMs` is a DURATION from now (forwarded as `delegateTo(…, { expiry: expiryMs })`), from
1318
+ * `mintRemainingMs`. Absent means Phase 1 behaviour (session-tier default).
1319
+ */
1320
+ options?: {
1321
+ expiryMs?: number;
1322
+ }): Promise<{
1323
+ ucan: string;
1324
+ parentCid: string;
1325
+ expiresAt: number;
1326
+ }>;
1327
+ /**
1328
+ * Finite consent anchor from an independently validated signed owner SIWE `issuedAt`
1329
+ * with finite signed `expirationTime` and an exact-full-space verified, uncaveated KV
1330
+ * get+sync witness, or the independently verified **oldest owner-authorizing** compact
1331
+ * proof's signed `iat`/`nbf` (never its child leaf). Null when owner proof bytes,
1332
+ * KV witness, finite SIWE expiration or proof timestamp are unavailable: arbitrary
1333
+ * did:key/empty-prf leaves and compact leaf CID/prf alone are insufficient even with
1334
+ * finite leaf `iat`/`nbf`. Storage may use `first_install` only to bound expiry;
1335
+ * null cannot establish a fresh session stamp.
1336
+ */
1337
+ consentAnchor?(): ReplicaConsentAnchor | null;
1338
+ /** True only for verified owner session-key derivation. Compact delegated / sessionOnly cannot silently renew. */
1339
+ readonly canRenew?: boolean;
1340
+ }
1341
+ interface ReplicationScheduler {
1342
+ now(): number;
1343
+ setTimeout(fn: () => void, ms: number): () => void;
1344
+ }
1345
+ interface KVReplicationDeps {
1346
+ options: ResolvedReplicationOptions;
1347
+ mode: "background" | "foreground";
1348
+ storage: KVReplicaStorage;
1349
+ identity: ReplicationIdentity;
1350
+ session: {
1351
+ id: string;
1352
+ did: string;
1353
+ space: string;
1354
+ };
1355
+ authority: ReplicationAuthority;
1356
+ /**
1357
+ * The controller MUST enforce the catch-up trust invariant. With durable pending state,
1358
+ * committedEpoch and records survive restarts, so the syncedThroughEpoch fence applies normally.
1359
+ * With non-durable state, a replica may serve local reads only after a successful sync that
1360
+ * started in this process after this store was created. Until then, every read MUST use the
1361
+ * network with REPLICA_UNPROVEN_SINCE_START; an older durable syncedThroughEpoch is not proof
1362
+ * after restart. Pending-state failures also fail closed to network reads.
1363
+ */
1364
+ pending: PendingWriteStore;
1365
+ /** Node `/info` features discovered on sign-in for protocol gating. */
1366
+ nodeFeatures?: readonly string[];
1367
+ /**
1368
+ * This session's stamp; forwarded into every `KVReplicaSpec.stamp` and pending store. With
1369
+ * `storage.lifecycle` present, a missing stamp makes the controller `gated` (`not_established`)
1370
+ * and it never opens. A controller is never rebound with a new stamp for an existing session
1371
+ * object: a new stamp requires a new session.
1372
+ */
1373
+ stamp?: ReplicaSessionStamp;
1374
+ scheduler: ReplicationScheduler;
1375
+ emit(event: ReplicationEvent): void;
1376
+ fetch?: FetchFunction;
1377
+ previous?: Promise<void>;
1378
+ }
1379
+ interface KVListPage {
1380
+ keys: string[];
1381
+ truncated?: boolean;
1382
+ nextCursor?: string;
1383
+ }
1384
+ interface KVReadThrough {
1385
+ /** The callback executes the complete existing network implementation under the caller's signal. */
1386
+ get<T>(r: {
1387
+ space: string;
1388
+ key: string;
1389
+ path: string;
1390
+ options: KVGetOptions | undefined;
1391
+ signal: AbortSignal;
1392
+ network: () => Promise<Result<KVResponse<T>>>;
1393
+ }): Promise<Result<KVResponse<T>>>;
1394
+ list(r: {
1395
+ space: string;
1396
+ listPath: string;
1397
+ options: KVListOptions | undefined;
1398
+ signal: AbortSignal;
1399
+ network: () => Promise<Result<KVListPage>>;
1400
+ }): Promise<Result<KVListPage>>;
1401
+ write<T>(r: {
1402
+ op: "put" | "delete" | "batchPut";
1403
+ space: string;
1404
+ entries: ReadonlyArray<{
1405
+ path: string;
1406
+ body?: Blob | string;
1407
+ }>;
1408
+ signal: AbortSignal;
1409
+ conditional?: boolean;
1410
+ network: () => Promise<Result<T>>;
1411
+ }): Promise<Result<T>>;
1412
+ /**
1413
+ * Best-effort observation for a caller-forced network read, invoked after it settles. This
1414
+ * synchronous event hook MUST NOT read replica storage or start a sync. KVService catches and
1415
+ * ignores thrown errors so the observation cannot change the network read.
1416
+ */
1417
+ observeNetworkRequested(r: {
1418
+ op: "get" | "list";
1419
+ space: string;
1420
+ /** Absolute get path, or the fully resolved list path. */
1421
+ path: string;
1422
+ reason: "NETWORK_REQUESTED";
1423
+ outcome: "found" | "not_found" | "error";
1424
+ latencyMs: number;
1425
+ }): void;
1426
+ }
1427
+ interface KVReplicationController extends KVReadThrough {
1428
+ status(): Promise<ReplicaStatusEntry[]>;
1429
+ sync(o?: {
1430
+ prefix?: string;
1431
+ signal?: AbortSignal;
1432
+ }): Promise<void>;
1433
+ /** Not async: synchronously stops timers/syncs and blocks new local opens before returning its promise. */
1434
+ purge(o?: {
1435
+ timeoutMs?: number;
1436
+ }): Promise<ReplicationPurgeReport>;
1437
+ clearPending(): Promise<number>;
1438
+ /** Synchronously aborts syncs; resolves after settlement and handle closure. */
1439
+ close(): Promise<void>;
1440
+ }
1441
+ interface ReplicationControl {
1442
+ status(): Promise<ReplicaStatusEntry[]>;
1443
+ sync(o?: {
1444
+ prefix?: string;
1445
+ }): Promise<void>;
1446
+ /**
1447
+ * Synchronous stop phase; promise never rejects. Awaits aborted syncs (bounded by timeout),
1448
+ * closes handles, purges every configured prefix of the bound/last identity, and clears only
1449
+ * records beneath successfully purged prefixes. Failures are returned and emitted.
1450
+ */
1451
+ purge(o?: {
1452
+ timeoutMs?: number;
1453
+ }): Promise<ReplicationPurgeReport>;
1454
+ /**
1455
+ * Removes all ambiguous records and in-flight records older than ten minutes; younger in-flight
1456
+ * and committed records remain. Exposed callers MUST show CLEAR_PENDING_WARNING first.
1457
+ */
1458
+ clearPending(): Promise<number>;
1459
+ close(): Promise<void>;
1460
+ }
1461
+ interface ReplicationOptions {
1462
+ enabled: boolean;
1463
+ /** Non-empty list of folder prefixes ending in `/`, or `/` for the whole space. */
1464
+ prefixes: string[];
1465
+ /** Required for prefixes/spaces where requiresSecretsOptIn returns true. */
1466
+ allowSecrets?: boolean;
1467
+ /** Background interval; default 60 seconds. */
1468
+ syncIntervalMs?: number;
1469
+ maxStalenessMs?: number;
1470
+ staleSyncTimeoutMs?: number;
1471
+ verify?: boolean;
1472
+ onEvent?: (event: ReplicationEvent) => void;
1473
+ /**
1474
+ * Device-grant lifetime from install; default DEFAULT_GRANT_TTL_MS (24 h). Values ≤ 0 or above
1475
+ * MAX_GRANT_TTL_MS (7 d) are a TypeError at construction.
1476
+ */
1477
+ grantTtlMs?: number;
1478
+ /**
1479
+ * Replica byte budget; absent means unbounded. Browser: origin-wide across every partition.
1480
+ * Node/CLI: per profile (per storage dir). Forwarded to storage via `KVReplicaSpec.quota`.
1481
+ */
1482
+ maxBytes?: number;
1483
+ /** Browser free-space floor; default DEFAULT_MIN_FREE_BYTES (64 MiB). Ignored on node/CLI. */
1484
+ minFreeBytes?: number;
1485
+ }
1486
+ interface ResolvedReplicationOptions extends ReplicationOptions {
1487
+ syncIntervalMs: number;
1488
+ maxStalenessMs: number;
1489
+ staleSyncTimeoutMs: number;
1490
+ allowSecrets: boolean;
1491
+ verify: boolean;
1492
+ }
1493
+ interface ReplicationPurgeReport {
1494
+ purged: string[];
1495
+ failed: Array<{
1496
+ prefix: string;
1497
+ code: string;
1498
+ }>;
1499
+ }
1500
+ interface PinnedKey {
1501
+ key: string;
1502
+ state: "in_flight" | "ambiguous";
1503
+ op: "put" | "delete";
1504
+ since: string;
1505
+ code?: string;
1506
+ likelyOrphaned: boolean;
1507
+ }
1508
+ interface ReplicaStatusEntry extends Partial<LocalReplicaStatus> {
1509
+ /** True when only opaque, ungated durable controls were inspected; no authority is known. */
1510
+ redacted?: boolean;
1511
+ prefix: string;
1512
+ state: ReplicaState;
1513
+ reason?: ReplicationReason;
1514
+ /** Set while state is `gated`. */
1515
+ gate?: ReplicaGateReason;
1516
+ errorCode?: string;
1517
+ pending: {
1518
+ inFlight: number;
1519
+ committed: number;
1520
+ ambiguous: number;
1521
+ };
1522
+ pinned: PinnedKey[];
1523
+ lagMs: number | null;
1524
+ }
1525
+ type ReplicaState = "idle" | "ready" | "grant_missing" | "runtime_unsupported" | "unavailable" | "revoked"
1526
+ /** The lifecycle gate refuses this session (REPLICA_GATED); reads use the network. */
1527
+ | "gated" | "closed";
1528
+ type ReplicationReason = "hit" | "absent" | "deleted" | "content_missing" | "coverage_incomplete" | "not_covered" | "pending_write" | "grant_missing" | "grant_expired" | "grant_revoked" | "grant_not_yet_valid" | "stale" | "replica_error" | "runtime_unsupported" | "replica_unavailable" | "unsupported_option" | "network_cursor" | "cursor_restart" | "aborted" | "REPLICA_BEHIND_OWN_WRITES" | "REPLICA_UNPROVEN_SINCE_START" | "NETWORK_REQUESTED"
1529
+ /** The lifecycle gate refused (REPLICA_GATED); the read went to the network. */
1530
+ | "replica_gated";
1531
+ type ReplicationEvent = {
1532
+ type: "replication.read";
1533
+ at: string;
1534
+ op: "get" | "list";
1535
+ space: string;
1536
+ key: string;
1537
+ replica: string | null;
1538
+ source: "replica" | "network" | "none";
1539
+ reason: ReplicationReason;
1540
+ outcome: "found" | "not_found" | "error";
1541
+ latencyMs: number;
1542
+ stalenessMs: number | null;
1543
+ coverage: ReplicaCoverage | null;
1544
+ authority: ReplicaAuthorityState | null;
1545
+ code?: string;
1546
+ syncError?: string;
1547
+ syncedBeforeRead?: boolean;
1548
+ pendingState?: "in_flight" | "committed" | "ambiguous";
1549
+ verify?: "match" | "diverged" | "error" | "aborted";
1550
+ count?: number;
1551
+ } | {
1552
+ type: "replication.write";
1553
+ at: string;
1554
+ op: "put" | "delete" | "batchPut";
1555
+ space: string;
1556
+ keys: string[];
1557
+ outcome: "committed" | "failed" | "ambiguous";
1558
+ code?: string;
1559
+ latencyMs: number;
1560
+ } | {
1561
+ type: "replication.sync";
1562
+ at: string;
1563
+ space: string;
1564
+ replica: string;
1565
+ /** `peer`: another tab/process committed (via `KVReplicaSpec.onPeerCommit`); no local work ran. */
1566
+ trigger: "start" | "interval" | "stale_read" | "manual" | "peer";
1567
+ /** Present and `peer` only for peer-commit notifications. */
1568
+ origin?: "peer";
1569
+ outcome: "ok" | "busy" | "error" | "aborted";
1570
+ class?: "offline" | "authority" | "storage" | "node" | "lifecycle";
1571
+ code?: string;
1572
+ durationMs: number;
1573
+ lagMs: number | null;
1574
+ pendingCleared?: number;
1575
+ pages?: number;
1576
+ changes?: number;
1577
+ deleted?: number;
1578
+ fetched?: number;
1579
+ contentMissing?: number;
1580
+ coverage?: ReplicaCoverage;
1581
+ } | {
1582
+ type: "replication.state";
1583
+ at: string;
1584
+ space?: string;
1585
+ replica?: string;
1586
+ state: "opened" | "grant_installed" | "grant_missing" | "runtime_unsupported" | "unavailable" | "revoked" | "recreated" | "purged" | "purge_failed" | "pending_store_error" | "pinned" | "pending_cleared" | "closed";
1587
+ code?: string;
1588
+ strategy?: "session" | "minted" | "installed";
1589
+ count?: number;
1590
+ keys?: string[];
1591
+ } | {
1592
+ /**
1593
+ * Grant lifetime and renewal (CONTRACT.md §3). Carries no raw account fields (`space`,
1594
+ * prefix or principal). This does not claim confidentiality against a raw browser-profile
1595
+ * reader who also has the persisted signed-in session.
1596
+ */
1597
+ type: "replication.grant";
1598
+ at: string;
1599
+ /** Opaque id from an opened `ReplicaGatedHandle`, never inferred from raw space/prefix. */
1600
+ replicaId: string;
1601
+ action: "minted" | "renewed"
1602
+ /** The node or authority refused renewal; current grant serves until effective expiry. */
1603
+ | "renewal_refused"
1604
+ /** Offline or the authority can't derive; retried on the next trigger. */
1605
+ | "renewal_deferred"
1606
+ /** An overlong session/restored grant was capped locally or replaced by a derived grant. */
1607
+ | "capped" | "expired" | "consent_deadline_reached";
1608
+ code?: string;
1609
+ /** ISO instants. */
1610
+ effectiveExpiresAt?: string;
1611
+ consentDeadline?: string;
1612
+ /** Duration passed to `mint` (`expiryMs`). */
1613
+ expiryMs?: number;
1614
+ } | {
1615
+ /** Sign-out / switch / consent-expiry lifecycle (CONTRACT.md §2). No raw account fields. */
1616
+ type: "replication.lifecycle";
1617
+ at: string;
1618
+ /** Replica id when the step concerns one partition (same scheme as `replication.grant`). */
1619
+ replicaId?: string;
1620
+ action: "denied"
1621
+ /** A stale caller's deny: disabled in memory only; gate untouched. */
1622
+ | "deny_stale" | "deny_failed" | "database_deleted" | "marker_set" | "marker_cleared" | "unguarded" | "drained" | "erased" | "erase_failed" | "activation_blocked" | "activated" | "recovered";
1623
+ reason?: "sign_out" | "account_switch" | "consent_expired" | "startup";
1624
+ gate?: ReplicaGateReason;
1625
+ /** Partitions/intents affected by this step. */
1626
+ partitions?: number;
1627
+ /** Intents still outstanding after this step. */
1628
+ remaining?: number;
1629
+ code?: string;
1630
+ } | {
1631
+ type: "replication.divergence";
1632
+ at: string;
1633
+ op: "get" | "list";
1634
+ space: string;
1635
+ key: string;
1636
+ replica: string;
1637
+ kind: "value" | "missing_local" | "extra_local" | "keys" | "order";
1638
+ localEtag?: string | null;
1639
+ networkEtag?: string | null;
1640
+ localOnly?: string[];
1641
+ networkOnly?: string[];
1642
+ stalenessMs: number | null;
1643
+ };
1644
+
1645
+ declare const CLEAR_PENDING_WARNING = "Clearing pending writes surrenders read-your-writes protection for the cleared keys. If a write whose record was cleared had an unknown outcome or never finished, it may commit now or later; this device can serve an older value until the next sync after that commit, as it would for a write from another device.";
1646
+
1647
+ /**
1648
+ * Pure canonicalization: lower-cases only EIP-155 address segments and preserves all other
1649
+ * space/principal bytes. Rejects non-HTTP(S) hosts, credentials, query/fragment, and non-full
1650
+ * TinyCloud space ids or non-pkh principals.
1651
+ */
1652
+ declare function canonicalReplicationIdentity(raw: ReplicationIdentity): ReplicationIdentity;
1653
+ /** Stable in-memory key; durable adapters hash its UTF-8 bytes with their platform crypto. */
1654
+ declare function replicationIdentityKey(id: ReplicationIdentity): string;
1655
+
1656
+ /** Whether replicating this space/prefix can copy encrypted secret material to disk. */
1657
+ declare function requiresSecretsOptIn(space: string, prefix: string): boolean;
1658
+
1659
+ export { type ReplicaQuotaLedger as $, type AuthorityRefusal as A, type ReplicaDevice as B, CLEAR_PENDING_WARNING as C, type ReplicaEraseReport as D, type ReplicaGateAccess as E, type ReplicaGateReason as F, type ReplicaGateScope as G, type ReplicaGateSnapshot as H, type ReplicaGateTx as I, type ReplicaGatedError as J, type KVReplicationDeps as K, type LocalGetResult as L, type ReplicaGatedHandle as M, type ReplicaGatedSpec as N, type ReplicaGatedStorage as O, type PendingWriteStore as P, type ReplicaGrantInfo as Q, type ReplicationIdentity as R, type ReplicaGuardedStore as S, type ReplicaInspectedStatus as T, type ReplicaLifecycleGate as U, type ReplicaPageAdmission as V, type ReplicaPartitionEraser as W, type ReplicaPartitionIndexEntry as X, type ReplicaPeerCommit as Y, type ReplicaPrivateGateHash as Z, type ReplicaPurgeIntent as _, type KVReplicationController as a, type ReplicaQuotaSpec as a0, type ReplicaSessionStamp as a1, type ReplicaStampedReply as a2, type ReplicaState as a3, type ReplicaStatusEntry as a4, type ReplicationAuthority as a5, type ReplicationControl as a6, type ReplicationEvent as a7, type ReplicationOptions as a8, type ReplicationPurgeReport as a9, type KVResponseHeaders as aA, type KVServiceConfig as aB, type KVSignedReadUrlResponse as aC, type ReplicationReason as aa, type ReplicationScheduler as ab, type ResolvedReplicationOptions as ac, type SaltedHash as ad, canonicalReplicationIdentity as ae, replicationIdentityKey as af, requiresSecretsOptIn as ag, DEFAULT_SIGNED_READ_URL_EXPIRY_MS as ah, KVAction as ai, type KVActionType as aj, type KVBatchPutItem as ak, type KVBatchPutOptions as al, type KVBatchPutResponse as am, type KVBatchReadResponse as an, type KVChange as ao, type KVChangesAuthority as ap, type KVChangesOptions as aq, type KVChangesResponse as ar, type KVCreateSignedReadUrlOptions as as, type KVDeleteOptions as at, type KVGetOptions as au, type KVHeadOptions as av, type KVListOptions as aw, type KVListResponse as ax, type KVPutOptions as ay, type KVResponse as az, type KVListPage as b, type KVReadThrough as c, type KVReplicaHandle as d, type KVReplicaSpec as e, type KVReplicaStorage as f, type LocalListResult as g, type LocalReadMeta as h, type LocalReplicaStatus as i, type LocalSyncResult as j, type PendingWriteRecord as k, type PendingWriteState as l, type PinnedKey as m, type PurgeTarget as n, type ReplicaAccountGate as o, type ReplicaAdmitHook as p, type ReplicaAuthorityState as q, type ReplicaBrowserGateAccess as r, type ReplicaBrowserGateSnapshot as s, type ReplicaBrowserGateTx as t, type ReplicaBrowserPageAdmission as u, type ReplicaClock as v, type ReplicaCoverage as w, type ReplicaDeliveryDecision as x, type ReplicaDenyInput as y, type ReplicaDenyOutcome as z };