@tinycloud/sdk-services 3.2.6-beta.2 → 3.3.0-beta.11

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 (65) hide show
  1. package/dist/{BaseService-4wwJq1Gn.d.cts → BaseService-Cc6Ueicm.d.cts} +1 -1
  2. package/dist/{BaseService-BGr2slUz.d.ts → BaseService-DOdXd7sr.d.ts} +1 -1
  3. package/dist/encryption/index.cjs +10 -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 +10 -0
  8. package/dist/encryption/index.js.map +1 -1
  9. package/dist/index.cjs +360 -210
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +116 -13
  12. package/dist/index.d.ts +116 -13
  13. package/dist/index.js +359 -210
  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 +68 -21
  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 +68 -21
  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/grantPolicy.cjs +82 -0
  30. package/dist/kv/replication/grantPolicy.cjs.map +1 -0
  31. package/dist/kv/replication/grantPolicy.d.cts +19 -0
  32. package/dist/kv/replication/grantPolicy.d.ts +19 -0
  33. package/dist/kv/replication/grantPolicy.js +50 -0
  34. package/dist/kv/replication/grantPolicy.js.map +1 -0
  35. package/dist/kv/replication/grantPolicyConstants.cjs +42 -0
  36. package/dist/kv/replication/grantPolicyConstants.cjs.map +1 -0
  37. package/dist/kv/replication/grantPolicyConstants.d.cts +10 -0
  38. package/dist/kv/replication/grantPolicyConstants.d.ts +10 -0
  39. package/dist/kv/replication/grantPolicyConstants.js +14 -0
  40. package/dist/kv/replication/grantPolicyConstants.js.map +1 -0
  41. package/dist/kv/replication/grantPolicyTypes.cjs +19 -0
  42. package/dist/kv/replication/grantPolicyTypes.cjs.map +1 -0
  43. package/dist/kv/replication/grantPolicyTypes.d.cts +87 -0
  44. package/dist/kv/replication/grantPolicyTypes.d.ts +87 -0
  45. package/dist/kv/replication/grantPolicyTypes.js +1 -0
  46. package/dist/kv/replication/grantPolicyTypes.js.map +1 -0
  47. package/dist/kv/replication/index.cjs +575 -131
  48. package/dist/kv/replication/index.cjs.map +1 -1
  49. package/dist/kv/replication/index.d.cts +6 -3
  50. package/dist/kv/replication/index.d.ts +6 -3
  51. package/dist/kv/replication/index.js +575 -131
  52. package/dist/kv/replication/index.js.map +1 -1
  53. package/dist/scope-B-m6cKH-.d.ts +1962 -0
  54. package/dist/scope-B7oeKdLM.d.cts +1962 -0
  55. package/dist/sql/index.cjs +72 -36
  56. package/dist/sql/index.cjs.map +1 -1
  57. package/dist/sql/index.d.cts +2 -2
  58. package/dist/sql/index.d.ts +2 -2
  59. package/dist/sql/index.js +72 -36
  60. package/dist/sql/index.js.map +1 -1
  61. package/dist/{types-D-9A3Sut.d.cts → types-BjH0ZtGy.d.cts} +8 -0
  62. package/dist/{types-D-9A3Sut.d.ts → types-BjH0ZtGy.d.ts} +8 -0
  63. package/package.json +55 -2
  64. package/dist/scope-CoKKmMiI.d.ts +0 -957
  65. package/dist/scope-D7wmjbMH.d.cts +0 -957
@@ -0,0 +1,1962 @@
1
+ import { R as Result, F as FetchFunction } from './types-BjH0ZtGy.js';
2
+ import { REPLICA_KV_STORES } from './kv/replication/contractConstants.js';
3
+ import { ReplicaConsentAnchor, ReplicaGrantPolicy } from './kv/replication/grantPolicyTypes.js';
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 committed 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 an ordinary denial deletes it
669
+ * through the identity's pending-intent erase before intent clear. Deferred node sign-out
670
+ * holds that intent and the device file until validated resume (CONTRACT.md §2).
671
+ */
672
+ {
673
+ status: "denied";
674
+ lifecycleEpoch: number;
675
+ intents: number;
676
+ }
677
+ /** The caller's principal didn't match the gate: disabled in memory only; gate untouched. */
678
+ | {
679
+ status: "stale";
680
+ }
681
+ /** Browser: deny commit failed; `deleteDatabase` succeeded, so nothing of any account remains. */
682
+ | {
683
+ status: "deleted";
684
+ }
685
+ /** Browser: deny and delete failed; logout marker written, delete retried on every load. */
686
+ | {
687
+ status: "marker";
688
+ code: string;
689
+ }
690
+ /** Nothing could be written. Cross-tab denial NOT guaranteed; this tab serves nothing. */
691
+ | {
692
+ status: "unguarded";
693
+ code: string;
694
+ };
695
+ /** A deferred sign-out denial: a committed `denied` always carries its resume token. */
696
+ type ProfileReplicaDeferredDenyOutcome = {
697
+ status: "denied";
698
+ lifecycleEpoch: number;
699
+ intents: number;
700
+ resumeToken: ProfileReplicaDeferredToken;
701
+ } | Exclude<ReplicaDenyOutcome, {
702
+ status: "denied";
703
+ }>;
704
+ interface ReplicaEraseReport {
705
+ erased: string[];
706
+ failed: Array<{
707
+ id: string;
708
+ code: string;
709
+ }>;
710
+ /** Intents still outstanding after this pass. */
711
+ remaining: number;
712
+ /**
713
+ * Node/CLI: a durable deferred denial held this pass, so nothing was erased and
714
+ * `remaining` (possibly 0) is not completion; check `deferredErase()`.
715
+ */
716
+ deferred?: true;
717
+ /** Browser startup discovered elapsed consent and durably crypto-shredded the account/gate keys. */
718
+ consentExpired?: boolean;
719
+ }
720
+ /**
721
+ * The structural guard (browser: one instance per transaction-owning context). Every
722
+ * session-bound transaction reading or writing `meta`, `entries`, `blobs`, `pending` or
723
+ * `device` MUST go through `run`: open, get/list, status, `grant()`, installGrant,
724
+ * page/repair/GC/sync completion and pending reads/updates. Narrow exceptions:
725
+ * purge/erase, pre-transaction key/control `snapshot()` and ungated bounded read-only
726
+ * `inspectStatus` (§1), which rechecks the gate key and reads only opaque numeric controls,
727
+ * never acquiring the account key, decrypting payloads or returning values/list keys.
728
+ */
729
+ interface ReplicaGateTx<Tx, Access extends ReplicaGateAccess = ReplicaGateAccess, Snapshot extends ReplicaGateSnapshot = ReplicaGateSnapshot> {
730
+ /**
731
+ * Opens the guarded operation. Browser: spans `scope.stores` plus all control stores
732
+ * (`gate`, `partitions`, `purgeIntents`, `quota`, `device`). The FIRST IDB request reads
733
+ * the gate; following control requests verify both stored nonextractable key records
734
+ * and intents **before any data request**. Compare snapshot incarnation/keyIds, stamp,
735
+ * principalHash and consent deadline; mismatch aborts before `fn` or writes. No WebCrypto
736
+ * await occurs inside the transaction. On active browser expiry, abort/read completion
737
+ * precedes an awaited main-thread deny/key-shred RPC; failed denial fails closed.
738
+ * `fn` awaits IDB requests only; readwrite uses strict durability and `run` resolves
739
+ * on IDB `oncomplete`.
740
+ *
741
+ * Node SQLite mutations hold the mandatory profile lock across rechecked gate and commit;
742
+ * reads check the gate and partition tombstone inside a SQLite read transaction. Node
743
+ * identity-scoped `pending.json` is not SQLite: reads/writes hold the profile lock across
744
+ * gate, intent, generation and file access (CONTRACT.md §2). Refusal rejects with
745
+ * {@link ReplicaGatedError}; an active browser deadline reports CONSENT_EXPIRED first.
746
+ */
747
+ run<T>(scope: ReplicaGateScope, access: Access, fn: (tx: Tx) => Promise<T>): Promise<T>;
748
+ /**
749
+ * Snapshot outside any guarded data transaction. Browser callers obtain the
750
+ * nonextractable gate-HMAC key and key ids, derive private hashes asynchronously,
751
+ * take the opaque Web Lock if needed, then `run` rechecks key ids/stamp before data.
752
+ * This control/key read never acquires the account key for ungated inspection.
753
+ * Node callers derive their salted principal hash before guarded work.
754
+ */
755
+ snapshot(): Promise<Snapshot | null>;
756
+ /** Registers a partition in the index, inside the `run` transaction that creates it. Idempotent. */
757
+ indexPartition(tx: Tx, entry: ReplicaPartitionIndexEntry): Promise<void>;
758
+ /** Removes the partition's index entry, inside its erase transaction. Idempotent. */
759
+ unindexPartition(tx: Tx, partitionId: string): Promise<void>;
760
+ /** Removes the intent `id`, in the same transaction as the erase it records (node: after it, see CONTRACT.md §2). */
761
+ clearIntent(tx: Tx, id: string): Promise<void>;
762
+ }
763
+ /** Browser guard: typed private snapshot and required prehashed/key-bound access. */
764
+ type ReplicaBrowserGateTx<Tx> = ReplicaGateTx<Tx, ReplicaBrowserGateAccess, ReplicaBrowserGateSnapshot>;
765
+ /**
766
+ * Erase seam implemented by the storage adapter, driven by the gate (worker-side in the browser).
767
+ * Browser erase and ledger accounting share an exempt IDB transaction with index/intent removal.
768
+ * Node partition erase uses SQLite tombstone/row deletion before unlink and intent clear; node
769
+ * pending identity erase runs under the profile lock over `pending.json` and `device.jwk`, not a
770
+ * SQLite transaction. All replay is idempotent.
771
+ */
772
+ interface ReplicaPartitionEraser<Tx> {
773
+ /**
774
+ * Browser: writes a keyless `purged: true` meta tombstone with only opaque pid/numeric
775
+ * controls (so open handles fail RESET_REQUIRED by generation), then removes entries,
776
+ * blobs, grant, config and authority. `bytes` is the old application-encoded rows
777
+ * minus the tombstone; its measured size stays charged until replacement/deletion,
778
+ * and replay returns zero without releasing it twice. Node: a SQLite tombstone
779
+ * fences open handles; its erasure has no browser quota-ledger semantics.
780
+ */
781
+ erasePartition(tx: Tx, partitionId: string): Promise<{
782
+ bytes: number;
783
+ }>;
784
+ /** Removes identity pending state (node: also `device.jwk`) durably before intent clear. */
785
+ eraseIdentityPending(tx: Tx, identityKeyHash: string): Promise<void>;
786
+ }
787
+ /**
788
+ * Main-thread facade over the platform gate, exposed as `KVReplicaStorage.lifecycle`. With
789
+ * replication on, storage MUST expose it (browser: the web runtime refuses storage without it with
790
+ * RUNTIME_UNSUPPORTED; node: `sqliteReplicaStorage` exposes it by default).
791
+ */
792
+ interface ReplicaLifecycleGate {
793
+ readonly kind: "indexeddb" | "file";
794
+ /**
795
+ * Startup recovery before anything binds: browser checks the durable consent deadline and,
796
+ * if expired, denies and crypto-shreds keys before any replica access; it also retries a
797
+ * pending `deleteDatabase` marker and erases intents. Failures remain gated and visible via
798
+ * `pendingPurge()` / `purgePending`; a browser closed at expiry catches it on its next load.
799
+ * Node/CLI recovery leaves durably deferred revoke intents and physical files untouched,
800
+ * reporting them as remaining until `ProfileReplicaPurge.resumeDeferredErase`.
801
+ */
802
+ recover(o: {
803
+ signal: AbortSignal;
804
+ }): Promise<ReplicaEraseReport>;
805
+ /**
806
+ * Fresh interactive sign-in ONLY. `consentAnchor` comes from independently validated
807
+ * signed owner SIWE with finite `expirationTime`, verified `issuedAt` and an exact-full-
808
+ * space uncaveated KV get+sync witness, or (for a compact-only session) the signed
809
+ * `iat`/`nbf` of the oldest independently verified owner-authorizing proof. A child
810
+ * leaf freshly minted under an old owner grant does not refresh consent; leaf
811
+ * signature/CID/audience alone do not prove it. An invalid/expirationless SIWE
812
+ * session does NOT fall through to a compact leaf or `first_install`. The anchor
813
+ * must be finite ms, clamped to now at capture and strictly newer than the last
814
+ * deny (browser: also the origin tombstone). Missing proof or non-finite anchors
815
+ * cannot establish (`not_established`); network sign-in/restore remain unaffected.
816
+ * Older anchors reject `stamp_stale`. Rejects
817
+ * `logout_pending` / `purge_pending` before mutation. Null principal → set it; matching
818
+ * principal → current stamp; another account → `principal_mismatch`.
819
+ */
820
+ establishSessionStamp(principal: string, o: {
821
+ consentAnchor: ReplicaConsentAnchor;
822
+ }): Promise<ReplicaSessionStamp>;
823
+ /**
824
+ * Restore ONLY; never mutates the gate. Returns `persisted` when it equals the gate's stamp and
825
+ * principal. Rejects `not_established` (no persisted stamp), `stamp_stale`, `principal_mismatch`,
826
+ * `logout_pending`, `purge_pending`.
827
+ */
828
+ restoreSessionStamp(principal: string, persisted: ReplicaSessionStamp | undefined): Promise<ReplicaSessionStamp>;
829
+ /** True only for a matching non-null gate principal, with no old intent or marker. A null principal first establishes a fresh session. */
830
+ activationAllowed(next: string): Promise<boolean>;
831
+ /**
832
+ * Synchronous, reason-bearing preflight before open, installGrant, get/list, sync or
833
+ * replica-derived event delivery. An elapsed browser deadline returns `consent_expired`
834
+ * before grant-expiry checks; the controller starts scoped denial and emits one grant
835
+ * deadline event per **opened** handle with a known opaque replicaId. Pre-open denial has
836
+ * no fabricated replica id and uses a lifecycle event/recovery report instead. A timer
837
+ * that already denied returns `gated` (`stamp_stale`). This does not mutate the gate.
838
+ */
839
+ checkDelivery(stamp: ReplicaSessionStamp): ReplicaDeliveryDecision;
840
+ /**
841
+ * Finite, immutable deadline from the persisted gate for this matching active stamp,
842
+ * synchronously available to the main-thread scheduler even when a restored compact
843
+ * session has no consent anchor. Null if stale, unavailable or not established; never
844
+ * derive a new deadline from `first_install` or the current session's leaf token.
845
+ * Browser timers call `checkDelivery` again before notifying/denying; gate checks on
846
+ * startup/open/read are mandatory independently of whether a timer ran. Node may
847
+ * return null when its gate has no persisted consent deadline.
848
+ */
849
+ consentDeadline(stamp: ReplicaSessionStamp): number | null;
850
+ /**
851
+ * Browser-only post-commit consent expiry observer. Fires only after deny/delete
852
+ * destroys the origin account keys; a subscriber failure cannot undo denial.
853
+ * Controllers register after binding so events use their opened opaque replica IDs.
854
+ */
855
+ subscribeConsentExpired?(listener: (stamp: ReplicaSessionStamp) => void): () => void;
856
+ /**
857
+ * Synchronous main-thread delivery check before replica-served values/list keys/event fields,
858
+ * and before open, installGrant and sync. Browser checks marker, origin deny tombstone,
859
+ * durable consent deadline against the same injected clock, and this tab's gate view; node
860
+ * checks its process view. No stamp means not deliverable.
861
+ */
862
+ deliverable(stamp: ReplicaSessionStamp): boolean;
863
+ /**
864
+ * Step 1. Flips the in-memory view before the first await. If the caller principal matches,
865
+ * one commit bumps epoch and deniedAt, records intents, advances pending generations and
866
+ * clears first-install anchors. Browser also deletes account AES-GCM and gate-HMAC keys, plus
867
+ * owner device, in that deny transaction BEFORE background erase; `consent_expired` uses this
868
+ * same path. Node deletes the device through the identity intent (CONTRACT.md §2). A stale
869
+ * principal disables only its own process/tab; the gate is untouched.
870
+ */
871
+ deny(input: ReplicaDenyInput): Promise<ReplicaDenyOutcome>;
872
+ /** Step 3: erase each outstanding intent in its own exempt transaction. */
873
+ erasePending(o: {
874
+ signal: AbortSignal;
875
+ }): Promise<ReplicaEraseReport>;
876
+ /** Outstanding intents; non-empty means status `purgePending: true`. */
877
+ pendingPurge(): Promise<ReplicaPurgeIntent[]>;
878
+ }
879
+ /** Node/CLI profile-local fencing token, not a secret capability; the profile lock remains authoritative. */
880
+ interface ProfileReplicaDeferredToken {
881
+ deniedEpoch: number;
882
+ principalHash: string;
883
+ }
884
+ /** Profile-relative, JSON-safe identity of a path listed before destructive logout. */
885
+ interface ProfileReplicaUnknownEntry {
886
+ path: string;
887
+ kind: "file" | "directory" | "symlink";
888
+ dev: string;
889
+ ino: string;
890
+ }
891
+ /** Profile-relative directory identity; dev/ino are decimal strings for durable JSON. */
892
+ interface ProfileReplicaRootIdentity {
893
+ path: string;
894
+ dev: string;
895
+ ino: string;
896
+ }
897
+ /** Exact nonsecret path identity displayed with an inventory confirmation token. */
898
+ interface ProfileReplicaApprovalEntry extends ProfileReplicaRootIdentity {
899
+ kind: ProfileReplicaUnknownEntry["kind"] | "missing";
900
+ }
901
+ /** Read-only profile-locked inventory, including legacy mirrors and flag-owned roots. */
902
+ interface ProfileReplicaPreflight {
903
+ knownLegacyReplicas: number;
904
+ unknownLegacyPaths: string[];
905
+ unknownPaths: string[];
906
+ unknownEntries: ProfileReplicaUnknownEntry[];
907
+ knownRoots: ProfileReplicaRootIdentity[];
908
+ knownEntries: ProfileReplicaUnknownEntry[];
909
+ missingDatabases: string[];
910
+ }
911
+ /**
912
+ * CLI-only durable crash fence. `principalDigest` is a domain-separated
913
+ * SHA-256 owner binding, not a secret or a substitute for the profile lock;
914
+ * v2/v3 never fabricate a scoped gate principal hash. A v1 `lifecycleEpoch`
915
+ * is the pre-denial epoch; `deniedEpoch` is the epoch the v1 denial actually
916
+ * committed, bound once after that denial and compared for equality from then
917
+ * on (CONTRACT.md §2). Remote-pending and remote-abandoned are mutually
918
+ * exclusive durable states.
919
+ */
920
+ type ProfileReplicaLogoutPending = ({
921
+ v: 1;
922
+ incarnation: string;
923
+ lifecycleEpoch: number;
924
+ principalHash: string;
925
+ deniedEpoch?: number;
926
+ } | {
927
+ v: 2;
928
+ scope: "orphan_profile";
929
+ nonce: string;
930
+ } | {
931
+ v: 3;
932
+ scope: "inactive_gate";
933
+ incarnation: string;
934
+ lifecycleEpoch: number;
935
+ }) & ProfileReplicaRemoteLogoutState & {
936
+ principalDigest: string;
937
+ /** Random durable 16-byte challenge salt as 32 lowercase hex characters. */
938
+ approvalSalt: string;
939
+ discardLocalChanges?: true;
940
+ approvedUnknown: ProfileReplicaUnknownEntry[];
941
+ knownRoots: ProfileReplicaRootIdentity[];
942
+ knownEntries: ProfileReplicaUnknownEntry[];
943
+ missingDatabases: string[];
944
+ };
945
+ /** A logout marker is remote-pending, remote-abandoned or neither; never both. */
946
+ type ProfileReplicaRemoteLogoutState = {
947
+ remotePending: true;
948
+ remoteAbandoned?: never;
949
+ } | {
950
+ remoteAbandoned: true;
951
+ remotePending?: never;
952
+ } | {
953
+ remotePending?: never;
954
+ remoteAbandoned?: never;
955
+ };
956
+ /**
957
+ * Node/CLI-only lifecycle facade for a profile's replica root. The node-sdk exports
958
+ * `createProfileReplicaPurge({ profileDir, guard })`; `guard` is the caller's
959
+ * mandatory profile lock and MUST be reentrant within one async call chain: the
960
+ * CLI calls the facade while already holding its (reentrant) profile lock. This
961
+ * does not widen the browser `ReplicaLifecycleGate`.
962
+ *
963
+ * While a durable deferred denial is active, `recover` and `erasePending`
964
+ * leave intents and physical files untouched; `pendingPurge` lists the intents.
965
+ * A complete enumeration may have zero intents, so `remaining: 0` does not
966
+ * prove completion: `deferredErase()` is the authoritative marker check, and
967
+ * `LocalReplicaStatus.purgePending` / `ReplicaInspectedStatus.purgePending`
968
+ * are true while the deferred marker or any intent remains. All guarded reads
969
+ * and new establishment remain refused. Deferred intents leave that state only
970
+ * through a validated resume (CONTRACT.md §2); there is no account-switch or
971
+ * consent-expiry override. The facade never denies or erases a gate,
972
+ * partition, pending state or device record whose active principal is not
973
+ * the caller's ("Logout by another account", CONTRACT.md §2).
974
+ */
975
+ interface ProfileReplicaPurge extends Pick<ReplicaLifecycleGate, "recover" | "erasePending" | "pendingPurge"> {
976
+ /**
977
+ * `deferErase: true` is valid only for `reason: "sign_out"`; other reasons throw
978
+ * TypeError before any mutation. A successful deferred `denied` outcome MUST carry
979
+ * `resumeToken`; stale or unguarded outcomes do not authorize a retryable revoke.
980
+ * Step 1 atomically denies, records every opaque partition/pending intent and
981
+ * pending generation, and persists the deferred marker. Physical files remain
982
+ * until `resumeDeferredErase`, even across process restart.
983
+ */
984
+ deny(input: ReplicaDenyInput & {
985
+ reason: "sign_out";
986
+ }, options: {
987
+ deferErase: true;
988
+ }): Promise<ProfileReplicaDeferredDenyOutcome>;
989
+ deny(input: ReplicaDenyInput, options?: {
990
+ deferErase?: false;
991
+ }): Promise<ReplicaDenyOutcome>;
992
+ /** Returns the persisted active token across restart, even with zero intents; null only after the marker clears. */
993
+ deferredErase(): Promise<ProfileReplicaDeferredToken | null>;
994
+ /**
995
+ * Under the profile lock, require the exact denied epoch and principal hash and a
996
+ * null gate principal; otherwise throw `REPLICA_GATED`/`stamp_stale` without erasure.
997
+ * Clear the deferred marker durably before idempotent erase. A crash after clearing
998
+ * is completed by ordinary recovery. Repeating the same token is safe only while
999
+ * the same denied epoch/hash persists; a successor establishment fences it.
1000
+ */
1001
+ resumeDeferredErase(token: ProfileReplicaDeferredToken, options: {
1002
+ signal: AbortSignal;
1003
+ }): Promise<ReplicaEraseReport>;
1004
+ /**
1005
+ * Read-only profile-locked inventory BEFORE marker, denial or remote call.
1006
+ * Report exact profile-relative unknown paths, kinds and dev/ino plus all
1007
+ * known roots/entries. Unknown/indeterminate data refuses unless explicit
1008
+ * discard consent was supplied; the CLI displays the inventory before acting.
1009
+ * A prior inventory is not a wildcard authorization for later new paths.
1010
+ */
1011
+ preflightLogout(input: {
1012
+ principal: string;
1013
+ discardLocalChanges?: boolean;
1014
+ }): Promise<ProfileReplicaPreflight>;
1015
+ /**
1016
+ * Re-inspect and compare the complete approved inventory under the profile
1017
+ * lock, then atomically fsync a scoped v1, orphan v2 or inactive v3 marker.
1018
+ * `approval` is required: the CLI passes its just-displayed preflight even
1019
+ * when it contains no unknown paths. With `remotePending`, the marker fences
1020
+ * replica reads but forbids credential cleanup or erasure until
1021
+ * `completeRemoteLogout` after all exact-CID revokes. Only approved
1022
+ * path/type/dev/ino identities and the explicit discard bit survive; changed
1023
+ * paths refuse pre-action. V1 active remote logout additionally requires a
1024
+ * committed deferred denial/token before online revoke; v2 orphan and v3
1025
+ * inactive markers instead use their version-specific gate-absence/epoch
1026
+ * fences.
1027
+ *
1028
+ * An existing marker is never rewritten here: the same owner, fence,
1029
+ * `remotePending` state and approved inventory return it unchanged; anything
1030
+ * else refuses without mutation. When the gate's active principal is the
1031
+ * caller's, it always yields a v1 marker, even with no replica roots or
1032
+ * intents; another account's active gate yields null and is never denied.
1033
+ * Null otherwise means there is no active principal and no replica-owned
1034
+ * work, not completed logout.
1035
+ *
1036
+ * A missing flag pending index, unrecognized child, nonempty pending state
1037
+ * or corrupt gate requires explicit discard. The CLI still fsyncs
1038
+ * authSignedOut then unlinks and fsyncs both credentials. CLI-bound reads
1039
+ * and ordinary network restore refuse any marker; an explicit remote-revoke
1040
+ * retry may use the retained key/session while `remotePending` without
1041
+ * delivering replica data.
1042
+ */
1043
+ prepareLogoutPending(input: {
1044
+ principal: string;
1045
+ approval: ProfileReplicaPreflight;
1046
+ discardLocalChanges?: boolean;
1047
+ remotePending?: true;
1048
+ }): Promise<ProfileReplicaLogoutPending | null>;
1049
+ /**
1050
+ * After every authoritative exact-CID revoke succeeds, revalidate the
1051
+ * marker's principal and version-specific fence under the profile lock:
1052
+ * v1 `deniedEpoch`/hash, v2 no replacement gate, or v3 exact inactive
1053
+ * epoch/null principal. Then durably remove `remotePending`. A marker that is
1054
+ * not remote-pending (remote-abandoned or already cleanup-ready) refuses
1055
+ * without mutation. A failed/partial remote sequence never calls this.
1056
+ */
1057
+ completeRemoteLogout(input: {
1058
+ principal: string;
1059
+ }): Promise<ProfileReplicaLogoutPending>;
1060
+ /**
1061
+ * Under the profile lock, report whether this principal's marker is still
1062
+ * remote-pending and its v1 deferred gate, v2 orphan or v3 inactive gate
1063
+ * fence still matches. False only when the marker is absent/completed;
1064
+ * a mismatched principal or stale gate refuses. This permits an explicit
1065
+ * network-only revoke retry, never replica delivery.
1066
+ */
1067
+ remotePendingLogout(input: {
1068
+ principal: string;
1069
+ }): Promise<boolean>;
1070
+ /**
1071
+ * First explicit PLAIN logout attempt after unfinished remote revoke:
1072
+ * compare the original durable approved inventory with the current
1073
+ * path/type/dev/ino set under the profile lock. Any change refuses with
1074
+ * `REPLICA_GATED`/`gate_unavailable`, prints every token-bound
1075
+ * `ProfileReplicaApprovalEntry`, exposes the same complete set as
1076
+ * `error.inventoryApprovalEntries` plus a nonsecret 32-hex-character
1077
+ * `error.inventoryApprovalToken`, and mutates nothing even when `approval`
1078
+ * contains the new paths or `discardLocalChanges` is true. With the
1079
+ * original inventory intact, validate the fresh approval and the marker's
1080
+ * version-specific owner/gate fence (a v1 marker is classified by its gate
1081
+ * and checked exactly one way, CONTRACT.md §2), then atomically replace
1082
+ * `remotePending` with `remoteAbandoned: true`.
1083
+ * This is not an online-success receipt and makes no further node call.
1084
+ */
1085
+ abandonRemoteLogout(input: {
1086
+ principal: string;
1087
+ approval: ProfileReplicaPreflight;
1088
+ discardLocalChanges?: boolean;
1089
+ }): Promise<ProfileReplicaLogoutPending>;
1090
+ /**
1091
+ * Only a separate explicit CLI command with
1092
+ * `--discard-local-changes --approve-inventory <token>` may approve a
1093
+ * changed remote-pending inventory. Under the profile lock, require the
1094
+ * same owner and version-specific gate fence, original-to-current inventory
1095
+ * mismatch, exact fresh preflight and matching displayed 32-hex-character
1096
+ * token. Commit the newly approved identities with `remoteAbandoned: true`,
1097
+ * never a remote receipt. Missing, stale or reused tokens fail without
1098
+ * mutation; there is no same-command fallback from `abandonRemoteLogout`.
1099
+ */
1100
+ approveChangedPlainLogout(input: {
1101
+ principal: string;
1102
+ approval: ProfileReplicaPreflight;
1103
+ discardLocalChanges: true;
1104
+ inventoryApprovalToken: string;
1105
+ }): Promise<ProfileReplicaLogoutPending>;
1106
+ /**
1107
+ * After a completed remote sequence, an inventory mismatch leaves the
1108
+ * marker and signed-out state in place. A fresh explicit listing/ack under
1109
+ * the profile lock replaces only the approved inventory; remote revokes
1110
+ * are not repeated, and a still-pending remote sequence cannot reapprove.
1111
+ */
1112
+ reapproveLogoutPending(input: {
1113
+ principal: string;
1114
+ approval: ProfileReplicaPreflight;
1115
+ }): Promise<ProfileReplicaLogoutPending>;
1116
+ /**
1117
+ * CLI startup/logout recovery under the same profile lock. A `remotePending`
1118
+ * marker returns failure with no credential or file deletion. For a
1119
+ * cleanup-ready active-v1 marker, validate the gate and durably commit
1120
+ * generation-fenced denial with complete partition/pending intents
1121
+ * BEFORE calling `clearCredentials`; denial failure leaves the marker,
1122
+ * both credentials and replica files intact and reports nonzero failure.
1123
+ * An already-deferred v1 validates its exact denial; v3 validates its
1124
+ * inactive epoch and v2 its orphan/no-replacement-gate fence without
1125
+ * inventing scoped intents. Only then does `clearCredentials` atomically
1126
+ * fsync CLI authSignedOut before unlinking and fsyncing both session.json
1127
+ * and key.json. Recheck durable approved identities before each unlink;
1128
+ * mismatch stops with marker, remaining files and nonzero failure.
1129
+ * Post-success credentials remain shredded. The callback erases only its
1130
+ * approved inventory under the profile lock (never recursive legacy
1131
+ * deletion). In-repo CLI code owns the signed-out postcondition; the
1132
+ * facade verifies credential absence before erasure. A missing marker
1133
+ * with corrupt/unreadable gate reports failure and nonzero remaining,
1134
+ * without invoking callbacks or fabricating successful completion.
1135
+ */
1136
+ recoverLogoutPending(options: {
1137
+ signal: AbortSignal;
1138
+ clearCredentials: () => Promise<void>;
1139
+ eraseLegacyReplicas: (options: {
1140
+ discardLocalChanges: boolean;
1141
+ approvedUnknown: readonly ProfileReplicaUnknownEntry[];
1142
+ knownRoots: readonly ProfileReplicaRootIdentity[];
1143
+ knownEntries: readonly ProfileReplicaUnknownEntry[];
1144
+ missingDatabases: readonly string[];
1145
+ }) => Promise<void>;
1146
+ }): Promise<ReplicaEraseReport>;
1147
+ }
1148
+ /** Worker replies that carry replica-served data are stamped; a reply without a stamp is dropped. */
1149
+ interface ReplicaStampedReply {
1150
+ stamp: ReplicaSessionStamp;
1151
+ }
1152
+
1153
+ /**
1154
+ * Quota contract (TC-858 Phase 2, CONTRACT.md §4). Types only.
1155
+ *
1156
+ * Browser: `maxBytes` is origin-wide over the deterministic application encoding of
1157
+ * ciphertext entries/blobs/meta and sealed owner-device plus grant/first-install device
1158
+ * records. The ledger lives in the `quota` control store; page and device writes admit
1159
+ * before any put, then account in the same guarded transaction. Deny releases the reserved
1160
+ * device bucket atomically with key deletion; partition erase remains idempotent.
1161
+ * Node/CLI: `maxBytes` is per profile (per storage `dir`); there is no ledger — usage is derived
1162
+ * from stored content, so erase replay is safe — and the `admit` hook runs inside `applyPage`'s
1163
+ * guarded mutation (profile lock held). `minFreeBytes` applies only in the browser.
1164
+ */
1165
+ /** Quota settings forwarded from `ReplicationOptions` to storage through `KVReplicaSpec.quota`. */
1166
+ interface ReplicaQuotaSpec {
1167
+ /** Byte budget; absent means unbounded (Phase 1 behaviour). */
1168
+ maxBytes?: number;
1169
+ /** Browser free-space floor; the browser adapter defaults it to `DEFAULT_MIN_FREE_BYTES`. */
1170
+ minFreeBytes?: number;
1171
+ }
1172
+ /** Node/CLI page admission keeps its existing blob/entry interpretation. */
1173
+ interface ReplicaPageAdmission {
1174
+ partitionId: string;
1175
+ /** Sum of the page's new blob bytes (node admission hook's existing metric). */
1176
+ blobBytes: number;
1177
+ /** Number of entry writes; not the browser ciphertext-row accounting metric. */
1178
+ entries: number;
1179
+ /** Optional caller-supplied free bytes; node's browser-only floor is not applied. */
1180
+ freeBytes: number | null;
1181
+ }
1182
+ /**
1183
+ * Browser page admission uses the exact signed net application-encoded byte change of
1184
+ * persisted ciphertext entries, blobs and active meta. Erase retains a keyless opaque/numeric
1185
+ * purged meta tombstone, charged to the partition until replacement or physical deletion;
1186
+ * the same row metric measures it. S5's browser ledger separately combines measured device-row
1187
+ * deltas for owner/stamp writes in a reserved device bucket. New rows are sealed before opening
1188
+ * the transaction; old stored rows are measured inside it. Replacements/GC may release space.
1189
+ * Node's page admission stays unchanged.
1190
+ */
1191
+ interface ReplicaBrowserPageAdmission {
1192
+ partitionId: string;
1193
+ deltaBytes: number;
1194
+ /** Free bytes read before opening the transaction; null when unknown. */
1195
+ freeBytes: number | null;
1196
+ }
1197
+ /**
1198
+ * Browser ledger seam, called by the adapter in guarded write or exempt erase/deny
1199
+ * transactions that include the `quota` store. Every sealed meta mutation (including
1200
+ * lease/status) and measured device write uses the origin ledger. Implementations MUST
1201
+ * only await requests on `tx`; S5's browser implementation adds combined-row methods
1202
+ * without changing this shared public interface.
1203
+ */
1204
+ interface ReplicaQuotaLedger<Tx> {
1205
+ /** Read outside any transaction, just before it: `navigator.storage.estimate()` quota − usage. */
1206
+ freeBytes(): Promise<number | null>;
1207
+ /**
1208
+ * Admits the exact projected application-encoded persisted ciphertext-row delta, including
1209
+ * sealed value/IV/tag plus persisted key/index fields and envelope framing. IDB engine/page
1210
+ * overhead is unobservable and excluded. The adapter uses the same measured delta for
1211
+ * `accountCommit`; never admits plaintext lengths or a per-entry estimate. On
1212
+ * STORAGE_BUDGET it aborts `tx` before any page write.
1213
+ */
1214
+ admitPage(tx: Tx, page: ReplicaBrowserPageAdmission): Promise<void>;
1215
+ /** Records the same measured signed delta in the page/repair/GC/meta transaction. */
1216
+ accountCommit(tx: Tx, o: {
1217
+ partitionId: string;
1218
+ deltaBytes: number;
1219
+ }): Promise<void>;
1220
+ /** Subtracts the exact encoded bytes removed in the erase transaction, idempotently. */
1221
+ accountErase(tx: Tx, o: {
1222
+ partitionId: string;
1223
+ bytes: number;
1224
+ }): Promise<void>;
1225
+ }
1226
+ /**
1227
+ * Node/CLI admission hook (`admit` on the SQLite store options), run inside `applyPage`'s guarded
1228
+ * mutation before any write. Rejects with code STORAGE_BUDGET to refuse the page.
1229
+ */
1230
+ type ReplicaAdmitHook = (page: ReplicaPageAdmission) => void | Promise<void>;
1231
+
1232
+ interface ReplicationIdentity {
1233
+ /** Canonical HTTP(S) URL: origin + path without trailing slash; no credentials/query/fragment. */
1234
+ host: string;
1235
+ /** Full TinyCloud space id; only the EIP-155 address segment is lower-cased. */
1236
+ space: string;
1237
+ /** Principal `did:pkh:eip155:…`; only the EIP-155 address segment is lower-cased. */
1238
+ principal: string;
1239
+ }
1240
+ type ReplicaCoverage = "empty" | "bootstrapping" | "complete";
1241
+ type ReplicaAuthorityState = "valid" | "expired" | "revoked" | "not-yet-valid";
1242
+ interface LocalReadMeta {
1243
+ asOf: string | null;
1244
+ coverage: ReplicaCoverage;
1245
+ authority: ReplicaAuthorityState;
1246
+ /** Durable identity commit epoch captured before the last successful sync began. */
1247
+ syncedThroughEpoch: number;
1248
+ }
1249
+ type LocalGetResult = {
1250
+ status: "present";
1251
+ key: string;
1252
+ value: Uint8Array;
1253
+ etag: string;
1254
+ metadata: Record<string, string>;
1255
+ meta: LocalReadMeta;
1256
+ } | {
1257
+ status: "content_missing";
1258
+ key: string;
1259
+ etag: string;
1260
+ metadata: Record<string, string>;
1261
+ meta: LocalReadMeta;
1262
+ } | {
1263
+ status: "deleted" | "absent" | "coverage_incomplete" | "not_covered";
1264
+ key: string;
1265
+ meta: LocalReadMeta;
1266
+ };
1267
+ /** Live keys under a plain `startsWith` prefix, after-exclusive UTF-8 byte ordering, at most limit. */
1268
+ interface LocalListResult {
1269
+ keys: string[];
1270
+ meta: LocalReadMeta;
1271
+ }
1272
+ type LocalSyncResult = {
1273
+ status: "synced";
1274
+ pages: number;
1275
+ changes: number;
1276
+ deleted: number;
1277
+ fetched: number;
1278
+ contentMissing: number;
1279
+ coverage: ReplicaCoverage;
1280
+ /** The input syncStartEpoch, persisted atomically only when this sync succeeds. */
1281
+ syncedThroughEpoch: number;
1282
+ } | {
1283
+ status: "busy";
1284
+ };
1285
+ interface ReplicaGrantInfo {
1286
+ cid: string;
1287
+ parentCid: string | null;
1288
+ /** The signed `exp`, ms since epoch; null when the grant has none. */
1289
+ expiresAt: number | null;
1290
+ /** Grant lifecycle: pending until confirmed/activated by a successful replica sync. */
1291
+ state: "active" | "pending";
1292
+ /** Both get and sync in the grant carry no caveats. */
1293
+ unconstrained: boolean;
1294
+ /**
1295
+ * ms since epoch, from the storage clock at the FIRST install of this grant CID, stored keyed
1296
+ * by CID and set once. Re-installing the same CID never refreshes it; recompute-at-open uses the
1297
+ * stored value.
1298
+ */
1299
+ installedAt?: number;
1300
+ /**
1301
+ * min(expiresAt, installedAt + grantTtlMs, consentDeadline), stored with the grant and
1302
+ * recomputed at open from the stored `installedAt` and the handle's `spec.grantPolicy`. Serving
1303
+ * requires `now < effectiveExpiresAt` with a finite stored value. Otherwise (NaN, ±Infinity,
1304
+ * missing) the effective authority is not `valid`, and get/list throw GRANT_EXPIRED inside the
1305
+ * read transaction without returning bytes or keys. Present on every Phase 2 grant.
1306
+ */
1307
+ effectiveExpiresAt?: number;
1308
+ /**
1309
+ * ms since epoch; consent anchor + CONSENT_WINDOW_MS. Serving and minting require
1310
+ * `now < consentDeadline` and a finite value (refusal code CONSENT_EXPIRED).
1311
+ */
1312
+ consentDeadline?: number;
1313
+ /**
1314
+ * The resolved consent anchor (finite ms, clamped to ≤ now). A `first_install` anchor is keyed by
1315
+ * identity (node: gate file `firstInstall`; browser: `device` store), survives partition reset,
1316
+ * purge, revocation and reinstall, and is cleared only by `deny`. An unreadable anchor fails
1317
+ * closed (no serving, no mint).
1318
+ */
1319
+ consentAnchor?: ReplicaConsentAnchor;
1320
+ }
1321
+ interface LocalReplicaStatus {
1322
+ coverage: ReplicaCoverage;
1323
+ lastSyncAt: string | null;
1324
+ /** Durable identity commit epoch captured before the last successful sync began. */
1325
+ syncedThroughEpoch: number;
1326
+ authority: {
1327
+ state: ReplicaAuthorityState;
1328
+ expiresAt: string | null;
1329
+ };
1330
+ grant: ReplicaGrantInfo | null;
1331
+ counts: {
1332
+ keys: number;
1333
+ contentMissing: number;
1334
+ tombstones: number;
1335
+ };
1336
+ bytes: number;
1337
+ lastError: {
1338
+ at: string;
1339
+ code: string;
1340
+ message: string;
1341
+ } | null;
1342
+ /**
1343
+ * Browser: result of `navigator.storage.persist()`; null when the API is missing or not yet
1344
+ * asked. Node/CLI omit it (disk storage is persistent).
1345
+ */
1346
+ persisted?: boolean | null;
1347
+ /** A purge intent covers this partition (or its identity); it is unreadable until erased. */
1348
+ purgePending?: boolean;
1349
+ }
1350
+ /**
1351
+ * Ungated browser inspection returns only opaque, unsealed numeric controls from the
1352
+ * v2 meta row: coverageCode, syncedThroughEpoch, keys/contentMissing/tombstones and
1353
+ * contentBytes. It must not acquire the account key or decrypt grant, authority, cursor,
1354
+ * errors or KV payload. `redacted: true` distinguishes unknown from absent/revoked.
1355
+ */
1356
+ interface ReplicaInspectedStatus {
1357
+ redacted: true;
1358
+ coverage: ReplicaCoverage;
1359
+ syncedThroughEpoch: number;
1360
+ counts: LocalReplicaStatus["counts"];
1361
+ bytes: number;
1362
+ persisted?: boolean | null;
1363
+ purgePending?: boolean;
1364
+ }
1365
+ interface ReplicaDevice {
1366
+ did: string;
1367
+ jwk: object;
1368
+ }
1369
+ /** Identity is canonical; `space` is the session's verbatim space id for replica calls. */
1370
+ interface KVReplicaSpec {
1371
+ identity: ReplicationIdentity;
1372
+ space: string;
1373
+ prefix: string;
1374
+ allowSecrets: boolean;
1375
+ /**
1376
+ * When present, adapters MUST use this device for partition naming, grant audience checks,
1377
+ * and signing sync requests; they MUST NOT load or create their own device key for this handle.
1378
+ */
1379
+ device?: ReplicaDevice;
1380
+ /** Graph-scoped fetch for main-thread adapters; worker adapters ignore it. */
1381
+ fetch?: FetchFunction;
1382
+ /**
1383
+ * The session stamp this handle is bound to; immutable for the handle's life. With a `lifecycle`
1384
+ * gate (mandatory when replication is on), a spec without it is refused REPLICA_GATED
1385
+ * `not_established`, and every guarded transaction runs through `ReplicaGateTx.run` with it.
1386
+ * Use {@link ReplicaGatedSpec} where the stamp must be present.
1387
+ */
1388
+ stamp?: ReplicaSessionStamp;
1389
+ /**
1390
+ * Grant lifetime policy. Mandatory when replication is on: the controller always sets it, and a
1391
+ * Phase 2 adapter rejects `open` without it (REPLICA_INVALID_ARGUMENT). Optional in the type only
1392
+ * so Phase 1 callers compile.
1393
+ */
1394
+ grantPolicy?: ReplicaGrantPolicy;
1395
+ /** Quota settings; absent means unbounded. */
1396
+ quota?: ReplicaQuotaSpec;
1397
+ /**
1398
+ * Called when another tab/process commits a sync for this partition (browser: the worker's
1399
+ * `committed` BroadcastChannel message). The controller emits `replication.sync` with
1400
+ * `trigger: "peer"`, `origin: "peer"`. Must not throw; must not be called for own commits.
1401
+ */
1402
+ onPeerCommit?(info: ReplicaPeerCommit): void;
1403
+ }
1404
+ interface ReplicaPeerCommit {
1405
+ /** ms since epoch of the peer's commit. */
1406
+ at: number;
1407
+ syncedThroughEpoch: number;
1408
+ changes?: number;
1409
+ coverage?: ReplicaCoverage;
1410
+ }
1411
+ /** A spec as a gated adapter accepts it: stamp and grant policy present. */
1412
+ type ReplicaGatedSpec = KVReplicaSpec & {
1413
+ stamp: ReplicaSessionStamp;
1414
+ grantPolicy: ReplicaGrantPolicy;
1415
+ };
1416
+ interface KVReplicaHandle {
1417
+ readonly spec: KVReplicaSpec;
1418
+ /** Always equals `spec.device.did` when an explicit device was provided. */
1419
+ readonly deviceDid: string;
1420
+ /**
1421
+ * Opaque partition id for events. Required on Phase 2 `ReplicaGatedHandle`; optional here
1422
+ * only for Phase 1 adapters. Browser: private gate-HMAC partition id; node: replica hash.
1423
+ * A controller MUST NOT derive one from raw prefix/space or emit a grant event without it.
1424
+ */
1425
+ readonly replicaId?: string;
1426
+ /**
1427
+ * Guarded read. Adapters MUST NOT return value bytes unless the effective authority is `valid`
1428
+ * (signed exp, effective expiry, consent deadline, node-attested authority, revocation): they
1429
+ * throw GRANT_EXPIRED / GRANT_REVOKED / GRANT_NOT_YET_VALID / CONSENT_EXPIRED inside the read
1430
+ * transaction, and the controller falls through to the network with the matching reason. An
1431
+ * erased partition rejects RESET_REQUIRED (by tombstone/generation, not by unlink).
1432
+ */
1433
+ get(key: string): Promise<LocalGetResult>;
1434
+ /** Guarded read; same authority rule as `get` (no keys unless authority is `valid`). */
1435
+ list(o: {
1436
+ prefix: string;
1437
+ after?: string;
1438
+ limit?: number;
1439
+ }): Promise<LocalListResult>;
1440
+ /** Guarded read (runs through `ReplicaGateTx.run`). */
1441
+ grant(): Promise<ReplicaGrantInfo | null>;
1442
+ /**
1443
+ * Installs a grant after validating audience (the handle's device), `kv/get` + `kv/sync` coverage
1444
+ * of the prefix, and no caveats (caveated grants reject GRANT_NOT_COVERING). Stores `installedAt`
1445
+ * on first install of the CID and `effectiveExpiresAt`. Guarded: REPLICA_GATED.
1446
+ */
1447
+ installGrant(ucan: string): Promise<ReplicaGrantInfo>;
1448
+ /**
1449
+ * The controller supplies syncStartEpoch, captured from this identity's committedEpoch before
1450
+ * starting the sync. Every adapter, including the browser worker, MUST persist that exact epoch
1451
+ * in the same transaction as the sync's completion marker, only after the whole sync finishes
1452
+ * (feed pages, repairs and GC). Failed, aborted and busy syncs, including a failure after the
1453
+ * last feed page, MUST leave the prior fence unchanged. A successful result returns that
1454
+ * persisted value as syncedThroughEpoch. Aborting `signal` stops at the last complete page;
1455
+ * nothing past it commits. Gated: REPLICA_GATED; quota refusal: STORAGE_BUDGET / STORAGE_FULL,
1456
+ * keeping committed content.
1457
+ */
1458
+ sync(o: {
1459
+ signal: AbortSignal;
1460
+ syncStartEpoch: number;
1461
+ }): Promise<LocalSyncResult>;
1462
+ /** Status reports the durable syncedThroughEpoch, including after reopening this handle. */
1463
+ status(): Promise<LocalReplicaStatus>;
1464
+ /** Cancels and awaits in-flight syncs before closing. */
1465
+ close(): Promise<void>;
1466
+ }
1467
+ /** Phase 2 opened handle: immutable stamp/policy and an opaque event identifier. */
1468
+ type ReplicaGatedHandle = KVReplicaHandle & {
1469
+ readonly spec: ReplicaGatedSpec;
1470
+ readonly replicaId: string;
1471
+ };
1472
+ interface PurgeTarget {
1473
+ identity: ReplicationIdentity;
1474
+ /** Verbatim session space id used by the corresponding replica handle. */
1475
+ space: string;
1476
+ prefix: string;
1477
+ /** Session device in delegate posture, when one is used. */
1478
+ sessionDeviceDid?: string;
1479
+ }
1480
+ /**
1481
+ * Replica persistence adapters. B-node's Node/CLI adapter MUST provide durable pending state.
1482
+ * If that state cannot be opened (for example, a file-store failure), it MUST surface the
1483
+ * unavailable/error to the controller; the controller MUST fail closed to network reads and
1484
+ * MUST NOT silently substitute memory.
1485
+ */
1486
+ interface KVReplicaStorage {
1487
+ readonly kind: "sqlite" | "indexeddb";
1488
+ open(spec: KVReplicaSpec): Promise<KVReplicaHandle>;
1489
+ /**
1490
+ * Read an existing replica's durable status without creating/mutating storage or waiting for a
1491
+ * mutation guard. Missing replicas return undefined; abort is honored. Ungated (no stamp):
1492
+ * browser returns a discriminated, redacted control-only status without acquiring the account
1493
+ * key or decrypting data; node/CLI may expose existing grant CIDs/times. Known metadata exposure.
1494
+ */
1495
+ inspectStatus?(spec: KVReplicaSpec, options: {
1496
+ signal: AbortSignal;
1497
+ }): Promise<LocalReplicaStatus | ReplicaInspectedStatus | undefined>;
1498
+ /**
1499
+ * Erases replicas for the identity and prefix across the deduplicated union of the adapter's
1500
+ * stored device (if any) and `sessionDeviceDid`. A missing stored device never skips the
1501
+ * session-device purge. Does not open, mint, or sync. Erasure removes entries, blobs, grant,
1502
+ * config, and authority; earlier handles reject with RESET_REQUIRED. A contentless tombstone
1503
+ * may remain. Rejects with an error carrying `code` on failure.
1504
+ */
1505
+ purge(target: PurgeTarget): Promise<void>;
1506
+ /**
1507
+ * Optional only for volatile runtimes. Omission means an in-memory store with `durable: false`.
1508
+ * B-node's Node/CLI adapter MUST provide a durable store and surface open failures, never
1509
+ * silently fall back to memory.
1510
+ *
1511
+ * With a `lifecycle` gate, the store is bound to `options.stamp` for its whole life (callers
1512
+ * cache stores per identity AND stamp). Every `read`, `readExisting` and `update` (append,
1513
+ * settle, remove, clear, flush) checks stamp, principal, incarnation and identity intent
1514
+ * (plus browser keyId) before touching storage; missing stamps refuse REPLICA_GATED
1515
+ * `not_established`. Browser derives private gate-HMAC ids asynchronously from `snapshot()`
1516
+ * before an IDB transaction, locks on that opaque identity id, then calls main-thread
1517
+ * `ReplicaGateTx.run`, whose first control request verifies the snapshot/keyId. Node's
1518
+ * identity-scoped `pending.json` is not SQLite: both reads and mutations take the mandatory
1519
+ * profile lock across gate/generation check and file access; mutations durably rename
1520
+ * before releasing it.
1521
+ * The identity generation advances atomically with a deny intent, so stale stores cannot
1522
+ * refill erased pending writes (CONTRACT.md §2). Missing locks fail closed.
1523
+ */
1524
+ pendingWrites?(identity: ReplicationIdentity, options?: {
1525
+ stamp?: ReplicaSessionStamp;
1526
+ }): PendingWriteStore;
1527
+ /**
1528
+ * The platform lifecycle gate (browser: origin DB control stores + logout marker; node/CLI:
1529
+ * gate file). Mandatory when replication is on: `indexedDbReplicaStorage()` and
1530
+ * `sqliteReplicaStorage()` expose it (node's `gateFile` defaults next to `dir`), and the web
1531
+ * runtime refuses storage without it with RUNTIME_UNSUPPORTED. Optional in the type only so
1532
+ * Phase 1 test doubles compile; use {@link ReplicaGatedStorage} for Phase 2 adapters.
1533
+ */
1534
+ readonly lifecycle?: ReplicaLifecycleGate;
1535
+ }
1536
+ /** A Phase 2 adapter: the unstamped Phase 1 `open` overload is deliberately absent. */
1537
+ type ReplicaGatedStorage = Omit<KVReplicaStorage, "open" | "lifecycle"> & {
1538
+ readonly lifecycle: ReplicaLifecycleGate;
1539
+ open(spec: ReplicaGatedSpec): Promise<ReplicaGatedHandle>;
1540
+ };
1541
+ interface PendingWriteRecord {
1542
+ opId: string;
1543
+ seq: number;
1544
+ key: string;
1545
+ op: "put" | "delete";
1546
+ state: "in_flight" | "committed" | "ambiguous";
1547
+ epoch: number | null;
1548
+ at: string;
1549
+ settledAt: string | null;
1550
+ code?: string;
1551
+ }
1552
+ /** One identity-scoped pending state, with a monotonically increasing commit epoch and sequence. */
1553
+ interface PendingWriteState {
1554
+ v: 2;
1555
+ identity: ReplicationIdentity;
1556
+ committedEpoch: number;
1557
+ seq: number;
1558
+ records: PendingWriteRecord[];
1559
+ }
1560
+ /** Identity-scoped pending state and its restart-durability guarantee. */
1561
+ interface PendingWriteStore {
1562
+ /**
1563
+ * True only when committedEpoch and all records survive process restarts; false for memory or
1564
+ * other volatile stores.
1565
+ */
1566
+ readonly durable: boolean;
1567
+ readonly identity: ReplicationIdentity;
1568
+ /**
1569
+ * Consistent gated snapshot. Browser: guarded IDB read; node/CLI: mandatory profile lock
1570
+ * across gate check and pending.json read (not an unlocked atomic-file snapshot).
1571
+ */
1572
+ read(): Promise<PendingWriteState>;
1573
+ /**
1574
+ * Read an existing durable snapshot without initializing storage; undefined means no file exists.
1575
+ * Gated.
1576
+ */
1577
+ readExisting?(): Promise<PendingWriteState | undefined>;
1578
+ /**
1579
+ * Serialized read-modify-write; durable stores persist before resolve. Errors on mismatch/failure.
1580
+ * Gated: the assert runs inside the same transaction/guard as the write.
1581
+ */
1582
+ update<T>(mutate: (s: PendingWriteState) => T): Promise<T>;
1583
+ }
1584
+ type AuthorityRefusal = {
1585
+ refused: "NOT_COVERED" | "CAVEATED_AUTHORITY" | "SESSION_EXPIRING";
1586
+ };
1587
+ interface ReplicationAuthority {
1588
+ /**
1589
+ * Compact delegated/session-only authority: never consult `plan`, mint or silently renew.
1590
+ * A matching persisted stamp and independently verified, active installed grant are not
1591
+ * enough to serve locally: the CURRENT session must also have uncaveated get authority
1592
+ * for each requested key/list prefix. If `sessionGrant(prefix)` refuses NOT_COVERED,
1593
+ * CAVEATED_AUTHORITY or SESSION_EXPIRING, use network KV even while that stored grant
1594
+ * remains active. A stored grant never establishes fresh consent or replaces the
1595
+ * current session's own authority.
1596
+ */
1597
+ sessionOnly?: boolean;
1598
+ sessionGrant(prefix: string): {
1599
+ ucan: string;
1600
+ device: ReplicaDevice;
1601
+ } | AuthorityRefusal;
1602
+ plan(prefix: string): {
1603
+ path: "session" | "runtime";
1604
+ parentCid: string;
1605
+ expiresAt: number;
1606
+ } | AuthorityRefusal;
1607
+ mint(deviceDid: string, prefix: string, signal: AbortSignal,
1608
+ /**
1609
+ * `expiryMs` is a DURATION from now (forwarded as `delegateTo(…, { expiry: expiryMs })`), from
1610
+ * `mintRemainingMs`. Absent means Phase 1 behaviour (session-tier default). `renewalOnly`
1611
+ * requires the authority to recheck a session-key parent at mint time; an initial runtime
1612
+ * parent mint leaves it unset.
1613
+ */
1614
+ options?: {
1615
+ expiryMs?: number;
1616
+ renewalOnly?: boolean;
1617
+ }): Promise<{
1618
+ ucan: string;
1619
+ parentCid: string;
1620
+ expiresAt: number;
1621
+ }>;
1622
+ /**
1623
+ * Finite consent anchor from an independently validated signed owner SIWE `issuedAt`
1624
+ * with finite signed `expirationTime` and an exact-full-space verified, uncaveated KV
1625
+ * get+sync witness, or the independently verified **oldest owner-authorizing** compact
1626
+ * proof's signed `iat`/`nbf` (never its child leaf). Null when owner proof bytes,
1627
+ * KV witness, finite SIWE expiration or proof timestamp are unavailable: arbitrary
1628
+ * did:key/empty-prf leaves and compact leaf CID/prf alone are insufficient even with
1629
+ * finite leaf `iat`/`nbf`. Storage may use `first_install` only to bound expiry;
1630
+ * null cannot establish a fresh session stamp.
1631
+ */
1632
+ consentAnchor?(): ReplicaConsentAnchor | null;
1633
+ /** True only for verified owner session-key derivation. Compact delegated / sessionOnly cannot silently renew. */
1634
+ readonly canRenew?: boolean;
1635
+ }
1636
+ interface ReplicationScheduler {
1637
+ now(): number;
1638
+ setTimeout(fn: () => void, ms: number): () => void;
1639
+ }
1640
+ interface KVReplicationDeps {
1641
+ options: ResolvedReplicationOptions;
1642
+ mode: "background" | "foreground";
1643
+ storage: KVReplicaStorage;
1644
+ identity: ReplicationIdentity;
1645
+ session: {
1646
+ id: string;
1647
+ did: string;
1648
+ space: string;
1649
+ };
1650
+ authority: ReplicationAuthority;
1651
+ /**
1652
+ * The controller MUST enforce the catch-up trust invariant. With durable pending state,
1653
+ * committedEpoch and records survive restarts, so the syncedThroughEpoch fence applies normally.
1654
+ * With non-durable state, a replica may serve local reads only after a successful sync that
1655
+ * started in this process after this store was created. Until then, every read MUST use the
1656
+ * network with REPLICA_UNPROVEN_SINCE_START; an older durable syncedThroughEpoch is not proof
1657
+ * after restart. Pending-state failures also fail closed to network reads.
1658
+ */
1659
+ pending: PendingWriteStore;
1660
+ /** Node `/info` features discovered on sign-in for protocol gating. */
1661
+ nodeFeatures?: readonly string[];
1662
+ /**
1663
+ * This session's stamp; forwarded into every `KVReplicaSpec.stamp` and pending store. With
1664
+ * `storage.lifecycle` present, a missing stamp makes the controller `gated` (`not_established`)
1665
+ * and it never opens. A controller is never rebound with a new stamp for an existing session
1666
+ * object: a new stamp requires a new session.
1667
+ */
1668
+ stamp?: ReplicaSessionStamp;
1669
+ scheduler: ReplicationScheduler;
1670
+ emit(event: ReplicationEvent): void;
1671
+ fetch?: FetchFunction;
1672
+ previous?: Promise<void>;
1673
+ }
1674
+ interface KVListPage {
1675
+ keys: string[];
1676
+ truncated?: boolean;
1677
+ nextCursor?: string;
1678
+ }
1679
+ interface KVReadThrough {
1680
+ /** The callback executes the complete existing network implementation under the caller's signal. */
1681
+ get<T>(r: {
1682
+ space: string;
1683
+ key: string;
1684
+ path: string;
1685
+ options: KVGetOptions | undefined;
1686
+ signal: AbortSignal;
1687
+ network: () => Promise<Result<KVResponse<T>>>;
1688
+ }): Promise<Result<KVResponse<T>>>;
1689
+ list(r: {
1690
+ space: string;
1691
+ listPath: string;
1692
+ options: KVListOptions | undefined;
1693
+ signal: AbortSignal;
1694
+ network: () => Promise<Result<KVListPage>>;
1695
+ }): Promise<Result<KVListPage>>;
1696
+ write<T>(r: {
1697
+ op: "put" | "delete" | "batchPut";
1698
+ space: string;
1699
+ entries: ReadonlyArray<{
1700
+ path: string;
1701
+ body?: Blob | string;
1702
+ }>;
1703
+ signal: AbortSignal;
1704
+ conditional?: boolean;
1705
+ network: () => Promise<Result<T>>;
1706
+ }): Promise<Result<T>>;
1707
+ /**
1708
+ * Best-effort observation for a caller-forced network read, invoked after it settles. This
1709
+ * synchronous event hook MUST NOT read replica storage or start a sync. KVService catches and
1710
+ * ignores thrown errors so the observation cannot change the network read.
1711
+ */
1712
+ observeNetworkRequested(r: {
1713
+ op: "get" | "list";
1714
+ space: string;
1715
+ /** Absolute get path, or the fully resolved list path. */
1716
+ path: string;
1717
+ reason: "NETWORK_REQUESTED";
1718
+ outcome: "found" | "not_found" | "error";
1719
+ latencyMs: number;
1720
+ }): void;
1721
+ }
1722
+ interface KVReplicationController extends KVReadThrough {
1723
+ /**
1724
+ * Internal browser gate observer: invoked only after the stamped durable consent-expiry
1725
+ * denial/deletion commits. Records opened opaque grant transitions even if the gate's own
1726
+ * timer wins before this controller's timer; does not issue a second deny.
1727
+ */
1728
+ observeConsentExpired(stamp: ReplicaSessionStamp): void;
1729
+ status(): Promise<ReplicaStatusEntry[]>;
1730
+ sync(o?: {
1731
+ prefix?: string;
1732
+ signal?: AbortSignal;
1733
+ }): Promise<void>;
1734
+ /** Not async: synchronously stops timers/syncs and blocks new local opens before returning its promise. */
1735
+ purge(o?: {
1736
+ timeoutMs?: number;
1737
+ }): Promise<ReplicationPurgeReport>;
1738
+ clearPending(): Promise<number>;
1739
+ /** Synchronously aborts syncs; resolves after settlement and handle closure. */
1740
+ close(): Promise<void>;
1741
+ }
1742
+ interface ReplicationControl {
1743
+ status(): Promise<ReplicaStatusEntry[]>;
1744
+ sync(o?: {
1745
+ prefix?: string;
1746
+ }): Promise<void>;
1747
+ /**
1748
+ * Synchronous stop phase; promise never rejects. Awaits aborted syncs (bounded by timeout),
1749
+ * closes handles, purges every configured prefix of the bound/last identity, and clears only
1750
+ * records beneath successfully purged prefixes. Failures are returned and emitted.
1751
+ */
1752
+ purge(o?: {
1753
+ timeoutMs?: number;
1754
+ }): Promise<ReplicationPurgeReport>;
1755
+ /**
1756
+ * Removes all ambiguous records and in-flight records older than ten minutes; younger in-flight
1757
+ * and committed records remain. Exposed callers MUST show CLEAR_PENDING_WARNING first.
1758
+ */
1759
+ clearPending(): Promise<number>;
1760
+ close(): Promise<void>;
1761
+ }
1762
+ interface ReplicationOptions {
1763
+ enabled: boolean;
1764
+ /** Non-empty list of folder prefixes ending in `/`, or `/` for the whole space. */
1765
+ prefixes: string[];
1766
+ /** Required for prefixes/spaces where requiresSecretsOptIn returns true. */
1767
+ allowSecrets?: boolean;
1768
+ /** Background interval; default 60 seconds. */
1769
+ syncIntervalMs?: number;
1770
+ maxStalenessMs?: number;
1771
+ staleSyncTimeoutMs?: number;
1772
+ verify?: boolean;
1773
+ onEvent?: (event: ReplicationEvent) => void;
1774
+ /**
1775
+ * Device-grant lifetime from install; default DEFAULT_GRANT_TTL_MS (24 h). Values ≤ 0 or above
1776
+ * MAX_GRANT_TTL_MS (7 d) are a TypeError at construction.
1777
+ */
1778
+ grantTtlMs?: number;
1779
+ /**
1780
+ * Replica byte budget; absent means unbounded. Browser: origin-wide across every partition.
1781
+ * Node/CLI: per profile (per storage dir). Forwarded to storage via `KVReplicaSpec.quota`.
1782
+ */
1783
+ maxBytes?: number;
1784
+ /** Browser free-space floor; default DEFAULT_MIN_FREE_BYTES (64 MiB). Ignored on node/CLI. */
1785
+ minFreeBytes?: number;
1786
+ }
1787
+ interface ResolvedReplicationOptions extends ReplicationOptions {
1788
+ syncIntervalMs: number;
1789
+ maxStalenessMs: number;
1790
+ staleSyncTimeoutMs: number;
1791
+ allowSecrets: boolean;
1792
+ verify: boolean;
1793
+ }
1794
+ interface ReplicationPurgeReport {
1795
+ purged: string[];
1796
+ failed: Array<{
1797
+ prefix: string;
1798
+ code: string;
1799
+ }>;
1800
+ }
1801
+ interface PinnedKey {
1802
+ key: string;
1803
+ state: "in_flight" | "ambiguous";
1804
+ op: "put" | "delete";
1805
+ since: string;
1806
+ code?: string;
1807
+ likelyOrphaned: boolean;
1808
+ }
1809
+ interface ReplicaStatusEntry extends Partial<LocalReplicaStatus> {
1810
+ /** True when only opaque, ungated durable controls were inspected; no authority is known. */
1811
+ redacted?: boolean;
1812
+ prefix: string;
1813
+ state: ReplicaState;
1814
+ reason?: ReplicationReason;
1815
+ /** Set while state is `gated`. */
1816
+ gate?: ReplicaGateReason;
1817
+ errorCode?: string;
1818
+ pending: {
1819
+ inFlight: number;
1820
+ committed: number;
1821
+ ambiguous: number;
1822
+ };
1823
+ pinned: PinnedKey[];
1824
+ lagMs: number | null;
1825
+ }
1826
+ type ReplicaState = "idle" | "ready" | "grant_missing" | "runtime_unsupported" | "unavailable" | "revoked"
1827
+ /** The lifecycle gate refuses this session (REPLICA_GATED); reads use the network. */
1828
+ | "gated" | "closed";
1829
+ type ReplicationReason = "hit" | "absent" | "deleted" | "content_missing" | "coverage_incomplete" | "not_covered" | "pending_write" | "grant_missing" | "grant_expired"
1830
+ /** A revoked grant, including a stale-read sync refusal; read-through uses network with code GRANT_REVOKED. */
1831
+ | "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"
1832
+ /** The lifecycle gate refused (REPLICA_GATED); the read went to the network. */
1833
+ | "replica_gated";
1834
+ type ReplicationEvent = {
1835
+ type: "replication.read";
1836
+ at: string;
1837
+ op: "get" | "list";
1838
+ space: string;
1839
+ key: string;
1840
+ replica: string | null;
1841
+ source: "replica" | "network" | "none";
1842
+ reason: ReplicationReason;
1843
+ outcome: "found" | "not_found" | "error";
1844
+ latencyMs: number;
1845
+ stalenessMs: number | null;
1846
+ coverage: ReplicaCoverage | null;
1847
+ authority: ReplicaAuthorityState | null;
1848
+ code?: string;
1849
+ syncError?: string;
1850
+ syncedBeforeRead?: boolean;
1851
+ pendingState?: "in_flight" | "committed" | "ambiguous";
1852
+ verify?: "match" | "diverged" | "error" | "aborted";
1853
+ count?: number;
1854
+ } | {
1855
+ type: "replication.write";
1856
+ at: string;
1857
+ op: "put" | "delete" | "batchPut";
1858
+ space: string;
1859
+ keys: string[];
1860
+ outcome: "committed" | "failed" | "ambiguous";
1861
+ code?: string;
1862
+ latencyMs: number;
1863
+ } | {
1864
+ type: "replication.sync";
1865
+ at: string;
1866
+ space: string;
1867
+ replica: string;
1868
+ /** `peer`: another tab/process committed (via `KVReplicaSpec.onPeerCommit`); no local work ran. */
1869
+ trigger: "start" | "interval" | "stale_read" | "manual" | "peer";
1870
+ /** Present and `peer` only for peer-commit notifications. */
1871
+ origin?: "peer";
1872
+ outcome: "ok" | "busy" | "error" | "aborted";
1873
+ class?: "offline" | "authority" | "storage" | "node" | "lifecycle";
1874
+ code?: string;
1875
+ durationMs: number;
1876
+ lagMs: number | null;
1877
+ pendingCleared?: number;
1878
+ pages?: number;
1879
+ changes?: number;
1880
+ deleted?: number;
1881
+ fetched?: number;
1882
+ contentMissing?: number;
1883
+ coverage?: ReplicaCoverage;
1884
+ } | {
1885
+ type: "replication.state";
1886
+ at: string;
1887
+ space?: string;
1888
+ replica?: string;
1889
+ state: "opened" | "grant_installed" | "grant_missing" | "runtime_unsupported" | "unavailable" | "revoked" | "recreated" | "purged" | "purge_failed" | "pending_store_error" | "pinned" | "pending_cleared" | "closed";
1890
+ code?: string;
1891
+ strategy?: "session" | "minted" | "installed";
1892
+ count?: number;
1893
+ keys?: string[];
1894
+ } | {
1895
+ /**
1896
+ * Grant lifetime and renewal (CONTRACT.md §3). Carries no raw account fields (`space`,
1897
+ * prefix or principal). This does not claim confidentiality against a raw browser-profile
1898
+ * reader who also has the persisted signed-in session.
1899
+ */
1900
+ type: "replication.grant";
1901
+ at: string;
1902
+ /** Opaque id from an opened `ReplicaGatedHandle`, never inferred from raw space/prefix. */
1903
+ replicaId: string;
1904
+ action: "minted" | "renewed"
1905
+ /** The node or authority refused renewal; current grant serves until effective expiry. */
1906
+ | "renewal_refused"
1907
+ /** Offline or the authority can't derive; retried on the next trigger. */
1908
+ | "renewal_deferred"
1909
+ /** An overlong session/restored grant was capped locally or replaced by a derived grant. */
1910
+ | "capped" | "expired" | "consent_deadline_reached";
1911
+ code?: string;
1912
+ /** ISO instants. */
1913
+ effectiveExpiresAt?: string;
1914
+ consentDeadline?: string;
1915
+ /** Duration passed to `mint` (`expiryMs`). */
1916
+ expiryMs?: number;
1917
+ } | {
1918
+ /** Sign-out / switch / consent-expiry lifecycle (CONTRACT.md §2). No raw account fields. */
1919
+ type: "replication.lifecycle";
1920
+ at: string;
1921
+ /** Replica id when the step concerns one partition (same scheme as `replication.grant`). */
1922
+ replicaId?: string;
1923
+ action: "denied"
1924
+ /** A stale caller's deny: disabled in memory only; gate untouched. */
1925
+ | "deny_stale" | "deny_failed" | "database_deleted" | "marker_set" | "marker_cleared" | "unguarded" | "drained" | "erased" | "erase_failed" | "activation_blocked" | "activated" | "recovered";
1926
+ reason?: "sign_out" | "account_switch" | "consent_expired" | "startup";
1927
+ gate?: ReplicaGateReason;
1928
+ /** Partitions/intents affected by this step. */
1929
+ partitions?: number;
1930
+ /** Intents still outstanding after this step. */
1931
+ remaining?: number;
1932
+ code?: string;
1933
+ } | {
1934
+ type: "replication.divergence";
1935
+ at: string;
1936
+ op: "get" | "list";
1937
+ space: string;
1938
+ key: string;
1939
+ replica: string;
1940
+ kind: "value" | "missing_local" | "extra_local" | "keys" | "order";
1941
+ localEtag?: string | null;
1942
+ networkEtag?: string | null;
1943
+ localOnly?: string[];
1944
+ networkOnly?: string[];
1945
+ stalenessMs: number | null;
1946
+ };
1947
+
1948
+ 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.";
1949
+
1950
+ /**
1951
+ * Pure canonicalization: lower-cases only EIP-155 address segments and preserves all other
1952
+ * space/principal bytes. Rejects non-HTTP(S) hosts, credentials, query/fragment, and non-full
1953
+ * TinyCloud space ids or non-pkh principals.
1954
+ */
1955
+ declare function canonicalReplicationIdentity(raw: ReplicationIdentity): ReplicationIdentity;
1956
+ /** Stable in-memory key; durable adapters hash its UTF-8 bytes with their platform crypto. */
1957
+ declare function replicationIdentityKey(id: ReplicationIdentity): string;
1958
+
1959
+ /** Whether replicating this space/prefix can copy encrypted secret material to disk. */
1960
+ declare function requiresSecretsOptIn(space: string, prefix: string): boolean;
1961
+
1962
+ export { type ReplicaGuardedStore as $, type AuthorityRefusal as A, type ReplicaBrowserGateAccess as B, CLEAR_PENDING_WARNING as C, type ReplicaBrowserGateSnapshot as D, type ReplicaBrowserGateTx as E, type ReplicaBrowserPageAdmission as F, type ReplicaClock as G, type ReplicaCoverage as H, type ReplicaDeliveryDecision as I, type ReplicaDenyInput as J, type KVReplicationDeps as K, type LocalGetResult as L, type ReplicaDenyOutcome as M, type ReplicaDevice as N, type ReplicaEraseReport as O, type PendingWriteStore as P, type ReplicaGateAccess as Q, type ReplicationIdentity as R, type ReplicaGateReason as S, type ReplicaGateScope as T, type ReplicaGateSnapshot as U, type ReplicaGateTx as V, type ReplicaGatedError as W, type ReplicaGatedHandle as X, type ReplicaGatedSpec as Y, type ReplicaGatedStorage as Z, type ReplicaGrantInfo as _, type KVReplicationController as a, type ReplicaInspectedStatus as a0, type ReplicaLifecycleGate as a1, type ReplicaPageAdmission as a2, type ReplicaPartitionEraser as a3, type ReplicaPartitionIndexEntry as a4, type ReplicaPeerCommit as a5, type ReplicaPrivateGateHash as a6, type ReplicaPurgeIntent as a7, type ReplicaQuotaLedger as a8, type ReplicaQuotaSpec as a9, type KVChangesResponse as aA, type KVCreateSignedReadUrlOptions as aB, type KVDeleteOptions as aC, type KVGetOptions as aD, type KVHeadOptions as aE, type KVListOptions as aF, type KVListResponse as aG, type KVPutOptions as aH, type KVResponse as aI, type KVResponseHeaders as aJ, type KVServiceConfig as aK, type KVSignedReadUrlResponse as aL, type ReplicaSessionStamp as aa, type ReplicaStampedReply as ab, type ReplicaState as ac, type ReplicaStatusEntry as ad, type ReplicationAuthority as ae, type ReplicationControl as af, type ReplicationEvent as ag, type ReplicationOptions as ah, type ReplicationPurgeReport as ai, type ReplicationReason as aj, type ReplicationScheduler as ak, type ResolvedReplicationOptions as al, type SaltedHash as am, canonicalReplicationIdentity as an, replicationIdentityKey as ao, requiresSecretsOptIn as ap, DEFAULT_SIGNED_READ_URL_EXPIRY_MS as aq, KVAction as ar, type KVActionType as as, type KVBatchPutItem as at, type KVBatchPutOptions as au, type KVBatchPutResponse as av, type KVBatchReadResponse as aw, type KVChange as ax, type KVChangesAuthority as ay, type KVChangesOptions 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 ProfileReplicaApprovalEntry as n, type ProfileReplicaDeferredDenyOutcome as o, type ProfileReplicaDeferredToken as p, type ProfileReplicaLogoutPending as q, type ProfileReplicaPreflight as r, type ProfileReplicaPurge as s, type ProfileReplicaRemoteLogoutState as t, type ProfileReplicaRootIdentity as u, type ProfileReplicaUnknownEntry as v, type PurgeTarget as w, type ReplicaAccountGate as x, type ReplicaAdmitHook as y, type ReplicaAuthorityState as z };