@tinycloud/sdk-services 3.2.6-beta.2 → 3.2.6-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseService-4wwJq1Gn.d.cts → BaseService-1Ug6y7DC.d.cts} +1 -1
- package/dist/{BaseService-BGr2slUz.d.ts → BaseService-Bn07Vudy.d.ts} +1 -1
- package/dist/encryption/index.cjs +2 -0
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/encryption/index.js +2 -0
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +15 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -11
- package/dist/index.d.ts +14 -11
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +2 -0
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +7 -4
- package/dist/kv/index.d.ts +7 -4
- package/dist/kv/index.js +2 -0
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/contractConstants.cjs +115 -0
- package/dist/kv/replication/contractConstants.cjs.map +1 -0
- package/dist/kv/replication/contractConstants.d.cts +77 -0
- package/dist/kv/replication/contractConstants.d.ts +77 -0
- package/dist/kv/replication/contractConstants.js +75 -0
- package/dist/kv/replication/contractConstants.js.map +1 -0
- package/dist/kv/replication/grantPolicyConstants.cjs +42 -0
- package/dist/kv/replication/grantPolicyConstants.cjs.map +1 -0
- package/dist/kv/replication/grantPolicyConstants.d.cts +10 -0
- package/dist/kv/replication/grantPolicyConstants.d.ts +10 -0
- package/dist/kv/replication/grantPolicyConstants.js +14 -0
- package/dist/kv/replication/grantPolicyConstants.js.map +1 -0
- package/dist/kv/replication/grantPolicyTypes.cjs +19 -0
- package/dist/kv/replication/grantPolicyTypes.cjs.map +1 -0
- package/dist/kv/replication/grantPolicyTypes.d.cts +87 -0
- package/dist/kv/replication/grantPolicyTypes.d.ts +87 -0
- package/dist/kv/replication/grantPolicyTypes.js +1 -0
- package/dist/kv/replication/grantPolicyTypes.js.map +1 -0
- package/dist/kv/replication/index.cjs +5 -4
- package/dist/kv/replication/index.cjs.map +1 -1
- package/dist/kv/replication/index.d.cts +6 -3
- package/dist/kv/replication/index.d.ts +6 -3
- package/dist/kv/replication/index.js +5 -4
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/scope-BnyVPZ5E.d.ts +1659 -0
- package/dist/scope-CQ5kmHlt.d.cts +1659 -0
- package/dist/sql/index.cjs +2 -0
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +2 -0
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-D-9A3Sut.d.cts → types-gmdUiorz.d.cts} +2 -0
- package/dist/{types-D-9A3Sut.d.ts → types-gmdUiorz.d.ts} +2 -0
- package/package.json +40 -1
- package/dist/scope-CoKKmMiI.d.ts +0 -957
- package/dist/scope-D7wmjbMH.d.cts +0 -957
|
@@ -0,0 +1,1659 @@
|
|
|
1
|
+
import { R as Result, F as FetchFunction } from './types-gmdUiorz.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 deny (browser: preserved across database recreation). */
|
|
530
|
+
deniedAt: number | null;
|
|
531
|
+
/** Browser: finite ms, fixed from verified fresh consent at establish; mandatory while active. */
|
|
532
|
+
consentDeadline?: number | null;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Node/CLI lowercase-hex HMAC-SHA-256, key = base64url-decoded salt, material = UTF-8.
|
|
536
|
+
* Browser identifiers MUST NOT use this public-salt helper.
|
|
537
|
+
*/
|
|
538
|
+
type SaltedHash = (salt: string, material: string) => Promise<string>;
|
|
539
|
+
/**
|
|
540
|
+
* Browser lowercase-hex HMAC-SHA-256 using a nonextractable gate key over UTF-8
|
|
541
|
+
* `${domain}\0${material}`. Derive outside an IDB transaction; domains separate principals,
|
|
542
|
+
* identities, partitions and owner lookups. The private key is deleted on deny/expiry.
|
|
543
|
+
*/
|
|
544
|
+
type ReplicaPrivateGateHash = (key: CryptoKey, domain: "principal" | "identity" | "partition" | "lookup", material: string) => Promise<string>;
|
|
545
|
+
/** Current gate incarnation, node salt and epoch; browser specialization also carries private key and ids. */
|
|
546
|
+
interface ReplicaGateSnapshot {
|
|
547
|
+
incarnation: string;
|
|
548
|
+
/** Node hashes with this value; browser never uses it for account identifiers. */
|
|
549
|
+
salt: string;
|
|
550
|
+
lifecycleEpoch: number;
|
|
551
|
+
}
|
|
552
|
+
/** Browser snapshot binds the nonextractable key and both key records to one gate generation. */
|
|
553
|
+
interface ReplicaBrowserGateSnapshot extends ReplicaGateSnapshot {
|
|
554
|
+
keyId: string;
|
|
555
|
+
accountKeyId: string;
|
|
556
|
+
gateHmac: CryptoKey;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Precomputed, stored opaque ids presented to one guarded operation. Derive principalHash
|
|
560
|
+
* before `run` (browser: private gate-HMAC from snapshot; node: salted hash before the
|
|
561
|
+
* profile-locked check). No raw principal or async WebCrypto computation inside the browser
|
|
562
|
+
* IDB transaction. `run` rechecks the snapshot's incarnation, stamp and stored principal hash.
|
|
563
|
+
*/
|
|
564
|
+
interface ReplicaGateAccess {
|
|
565
|
+
/** Required: no unstamped access when a gate exists. */
|
|
566
|
+
stamp: ReplicaSessionStamp;
|
|
567
|
+
incarnation: string;
|
|
568
|
+
principalHash: string;
|
|
569
|
+
/** Stored partition id; refused while a purge intent covers it. */
|
|
570
|
+
partitionId?: string;
|
|
571
|
+
/** Stored identity id; refused while a pending intent covers it. */
|
|
572
|
+
identityKeyHash?: string;
|
|
573
|
+
}
|
|
574
|
+
/** Browser guard requires both key ids; missing ids cannot silently compile or enter `run`. */
|
|
575
|
+
interface ReplicaBrowserGateAccess extends ReplicaGateAccess {
|
|
576
|
+
keyId: string;
|
|
577
|
+
accountKeyId: string;
|
|
578
|
+
}
|
|
579
|
+
/** Why the gate refused (`REPLICA_GATED` errors carry this as `gate`). */
|
|
580
|
+
type ReplicaGateReason =
|
|
581
|
+
/** No stamp: the session was never established, or a restore had no persisted stamp. */
|
|
582
|
+
"not_established"
|
|
583
|
+
/** Incarnation or lifecycleEpoch differs from the gate (signed out, switched, or DB recreated). */
|
|
584
|
+
| "stamp_stale"
|
|
585
|
+
/** The gate's principal is null or another account. */
|
|
586
|
+
| "principal_mismatch"
|
|
587
|
+
/** A purge intent covers the partition/identity, or activation awaits the previous principal's erase. */
|
|
588
|
+
| "purge_pending"
|
|
589
|
+
/** Browser only: the `localStorage` logout marker is set. */
|
|
590
|
+
| "logout_pending"
|
|
591
|
+
/** The gate store itself could not be read; fail closed. */
|
|
592
|
+
| "gate_unavailable";
|
|
593
|
+
/** Shape of every gate refusal error: `Error & { code: "REPLICA_GATED"; gate }`. */
|
|
594
|
+
interface ReplicaGatedError extends Error {
|
|
595
|
+
code: "REPLICA_GATED";
|
|
596
|
+
gate: ReplicaGateReason;
|
|
597
|
+
}
|
|
598
|
+
/** Main-thread synchronous preflight: the controller observes expiry even without a worker read. */
|
|
599
|
+
type ReplicaDeliveryDecision = {
|
|
600
|
+
status: "allow";
|
|
601
|
+
} | {
|
|
602
|
+
status: "consent_expired";
|
|
603
|
+
code: "CONSENT_EXPIRED";
|
|
604
|
+
} | {
|
|
605
|
+
status: "gated";
|
|
606
|
+
gate: ReplicaGateReason;
|
|
607
|
+
};
|
|
608
|
+
/** Store names a guarded transaction may touch (data stores, plus `device` for owner-device and anchor reads). */
|
|
609
|
+
type ReplicaGuardedStore = (typeof REPLICA_KV_STORES)["meta" | "entries" | "blobs" | "pending" | "device"];
|
|
610
|
+
/** Store set and mode of one guarded transaction. Control stores are always added by `run`. */
|
|
611
|
+
interface ReplicaGateScope {
|
|
612
|
+
stores: readonly ReplicaGuardedStore[];
|
|
613
|
+
mode: "readonly" | "readwrite";
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Every partition ever created in the gate's scope. Browser opaque IDs are private
|
|
617
|
+
* gate-HMAC outputs, never plaintext principal, prefix or device. `lookupId` is independent
|
|
618
|
+
* of sealed owner device DID and indexes one or more stored partition ids for ungated
|
|
619
|
+
* inspection/purge while the gate key exists; after deny, erasure uses recorded intents.
|
|
620
|
+
* The key in IDB is NOT protected from a raw browser-profile reader with a valid session.
|
|
621
|
+
* Node identity dirs retain their known unsalted base32 exposure (CONTRACT.md §1).
|
|
622
|
+
*/
|
|
623
|
+
interface ReplicaPartitionIndexEntry {
|
|
624
|
+
partitionId: string;
|
|
625
|
+
principalHash: string;
|
|
626
|
+
identityKeyHash: string;
|
|
627
|
+
/** `gateHash(key, "lookup", identityKey + "\n" + prefix)`; nonunique across devices. */
|
|
628
|
+
lookupId: string;
|
|
629
|
+
/** True only when the adapter minted/used its stored owner device (`spec.device` absent). */
|
|
630
|
+
ownerDevice: boolean;
|
|
631
|
+
/** ms since epoch. */
|
|
632
|
+
createdAt: number;
|
|
633
|
+
}
|
|
634
|
+
/** A partition (or an identity's pending-write record) that must be erased before anything reads it. */
|
|
635
|
+
type ReplicaPurgeIntent = {
|
|
636
|
+
id: string;
|
|
637
|
+
kind: "partition";
|
|
638
|
+
partitionId: string;
|
|
639
|
+
principalHash: string;
|
|
640
|
+
recordedAt: number;
|
|
641
|
+
} | {
|
|
642
|
+
id: string;
|
|
643
|
+
kind: "pending";
|
|
644
|
+
identityKeyHash: string;
|
|
645
|
+
principalHash: string;
|
|
646
|
+
recordedAt: number;
|
|
647
|
+
};
|
|
648
|
+
interface ReplicaDenyInput {
|
|
649
|
+
/** Every scoped denial clears the current gate principal, including account switch. */
|
|
650
|
+
reason: "sign_out" | "account_switch" | "consent_expired";
|
|
651
|
+
/**
|
|
652
|
+
* The caller's own principal. If it doesn't match the gate, this stale caller disables only
|
|
653
|
+
* itself in memory, leaving the gate and successor account untouched.
|
|
654
|
+
*/
|
|
655
|
+
principal: string;
|
|
656
|
+
/** Intended successor on switch, never stored under the old account/gate HMAC key. */
|
|
657
|
+
next?: string;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Result of step 1 (deny + intent). Never rejects: sign-out always succeeds in memory. Every
|
|
661
|
+
* outcome has already flipped this tab's/process's in-memory view (so `deliverable` is false)
|
|
662
|
+
* BEFORE `deny`'s first await.
|
|
663
|
+
*/
|
|
664
|
+
type ReplicaDenyOutcome =
|
|
665
|
+
/**
|
|
666
|
+
* Gate moved, intents recorded, first-install anchors cleared and pending generations advanced
|
|
667
|
+
* in one commit. Browser: owner device deleted in that same IDB transaction. Node: the gate
|
|
668
|
+
* file cannot commit atomically with the device file, so the device is durably deleted by its
|
|
669
|
+
* identity's pending-intent erase before that intent clears (idempotent replay).
|
|
670
|
+
*/
|
|
671
|
+
{
|
|
672
|
+
status: "denied";
|
|
673
|
+
lifecycleEpoch: number;
|
|
674
|
+
intents: number;
|
|
675
|
+
}
|
|
676
|
+
/** The caller's principal didn't match the gate: disabled in memory only; gate untouched. */
|
|
677
|
+
| {
|
|
678
|
+
status: "stale";
|
|
679
|
+
}
|
|
680
|
+
/** Browser: deny commit failed; `deleteDatabase` succeeded, so nothing of any account remains. */
|
|
681
|
+
| {
|
|
682
|
+
status: "deleted";
|
|
683
|
+
}
|
|
684
|
+
/** Browser: deny and delete failed; logout marker written, delete retried on every load. */
|
|
685
|
+
| {
|
|
686
|
+
status: "marker";
|
|
687
|
+
code: string;
|
|
688
|
+
}
|
|
689
|
+
/** Nothing could be written. Cross-tab denial NOT guaranteed; this tab serves nothing. */
|
|
690
|
+
| {
|
|
691
|
+
status: "unguarded";
|
|
692
|
+
code: string;
|
|
693
|
+
};
|
|
694
|
+
interface ReplicaEraseReport {
|
|
695
|
+
erased: string[];
|
|
696
|
+
failed: Array<{
|
|
697
|
+
id: string;
|
|
698
|
+
code: string;
|
|
699
|
+
}>;
|
|
700
|
+
/** Intents still outstanding after this pass. */
|
|
701
|
+
remaining: number;
|
|
702
|
+
/** Browser startup discovered elapsed consent and durably crypto-shredded the account/gate keys. */
|
|
703
|
+
consentExpired?: boolean;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* The structural guard (browser: one instance per transaction-owning context). Every
|
|
707
|
+
* session-bound transaction reading or writing `meta`, `entries`, `blobs`, `pending` or
|
|
708
|
+
* `device` MUST go through `run`: open, get/list, status, `grant()`, installGrant,
|
|
709
|
+
* page/repair/GC/sync completion and pending reads/updates. Narrow exceptions:
|
|
710
|
+
* purge/erase, pre-transaction key/control `snapshot()` and ungated bounded read-only
|
|
711
|
+
* `inspectStatus` (§1), which rechecks the gate key and reads only opaque numeric controls,
|
|
712
|
+
* never acquiring the account key, decrypting payloads or returning values/list keys.
|
|
713
|
+
*/
|
|
714
|
+
interface ReplicaGateTx<Tx, Access extends ReplicaGateAccess = ReplicaGateAccess, Snapshot extends ReplicaGateSnapshot = ReplicaGateSnapshot> {
|
|
715
|
+
/**
|
|
716
|
+
* Opens the guarded operation. Browser: spans `scope.stores` plus all control stores
|
|
717
|
+
* (`gate`, `partitions`, `purgeIntents`, `quota`, `device`). The FIRST IDB request reads
|
|
718
|
+
* the gate; following control requests verify both stored nonextractable key records
|
|
719
|
+
* and intents **before any data request**. Compare snapshot incarnation/keyIds, stamp,
|
|
720
|
+
* principalHash and consent deadline; mismatch aborts before `fn` or writes. No WebCrypto
|
|
721
|
+
* await occurs inside the transaction. On active browser expiry, abort/read completion
|
|
722
|
+
* precedes an awaited main-thread deny/key-shred RPC; failed denial fails closed.
|
|
723
|
+
* `fn` awaits IDB requests only; readwrite uses strict durability and `run` resolves
|
|
724
|
+
* on IDB `oncomplete`.
|
|
725
|
+
*
|
|
726
|
+
* Node SQLite mutations hold the mandatory profile lock across rechecked gate and commit;
|
|
727
|
+
* reads check the gate and partition tombstone inside a SQLite read transaction. Node
|
|
728
|
+
* identity-scoped `pending.json` is not SQLite: reads/writes hold the profile lock across
|
|
729
|
+
* gate, intent, generation and file access (CONTRACT.md §2). Refusal rejects with
|
|
730
|
+
* {@link ReplicaGatedError}; an active browser deadline reports CONSENT_EXPIRED first.
|
|
731
|
+
*/
|
|
732
|
+
run<T>(scope: ReplicaGateScope, access: Access, fn: (tx: Tx) => Promise<T>): Promise<T>;
|
|
733
|
+
/**
|
|
734
|
+
* Snapshot outside any guarded data transaction. Browser callers obtain the
|
|
735
|
+
* nonextractable gate-HMAC key and key ids, derive private hashes asynchronously,
|
|
736
|
+
* take the opaque Web Lock if needed, then `run` rechecks key ids/stamp before data.
|
|
737
|
+
* This control/key read never acquires the account key for ungated inspection.
|
|
738
|
+
* Node callers derive their salted principal hash before guarded work.
|
|
739
|
+
*/
|
|
740
|
+
snapshot(): Promise<Snapshot | null>;
|
|
741
|
+
/** Registers a partition in the index, inside the `run` transaction that creates it. Idempotent. */
|
|
742
|
+
indexPartition(tx: Tx, entry: ReplicaPartitionIndexEntry): Promise<void>;
|
|
743
|
+
/** Removes the partition's index entry, inside its erase transaction. Idempotent. */
|
|
744
|
+
unindexPartition(tx: Tx, partitionId: string): Promise<void>;
|
|
745
|
+
/** Removes the intent `id`, in the same transaction as the erase it records (node: after it, see CONTRACT.md §2). */
|
|
746
|
+
clearIntent(tx: Tx, id: string): Promise<void>;
|
|
747
|
+
}
|
|
748
|
+
/** Browser guard: typed private snapshot and required prehashed/key-bound access. */
|
|
749
|
+
type ReplicaBrowserGateTx<Tx> = ReplicaGateTx<Tx, ReplicaBrowserGateAccess, ReplicaBrowserGateSnapshot>;
|
|
750
|
+
/**
|
|
751
|
+
* Erase seam implemented by the storage adapter, driven by the gate (worker-side in the browser).
|
|
752
|
+
* Browser erase and ledger accounting share an exempt IDB transaction with index/intent removal.
|
|
753
|
+
* Node partition erase uses SQLite tombstone/row deletion before unlink and intent clear; node
|
|
754
|
+
* pending identity erase runs under the profile lock over `pending.json` and `device.jwk`, not a
|
|
755
|
+
* SQLite transaction. All replay is idempotent.
|
|
756
|
+
*/
|
|
757
|
+
interface ReplicaPartitionEraser<Tx> {
|
|
758
|
+
/**
|
|
759
|
+
* Browser: writes a keyless `purged: true` meta tombstone with only opaque pid/numeric
|
|
760
|
+
* controls (so open handles fail RESET_REQUIRED by generation), then removes entries,
|
|
761
|
+
* blobs, grant, config and authority. `bytes` is the old application-encoded rows
|
|
762
|
+
* minus the tombstone; its measured size stays charged until replacement/deletion,
|
|
763
|
+
* and replay returns zero without releasing it twice. Node: a SQLite tombstone
|
|
764
|
+
* fences open handles; its erasure has no browser quota-ledger semantics.
|
|
765
|
+
*/
|
|
766
|
+
erasePartition(tx: Tx, partitionId: string): Promise<{
|
|
767
|
+
bytes: number;
|
|
768
|
+
}>;
|
|
769
|
+
/** Removes identity pending state (node: also `device.jwk`) durably before intent clear. */
|
|
770
|
+
eraseIdentityPending(tx: Tx, identityKeyHash: string): Promise<void>;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Main-thread facade over the platform gate, exposed as `KVReplicaStorage.lifecycle`. With
|
|
774
|
+
* replication on, storage MUST expose it (browser: the web runtime refuses storage without it with
|
|
775
|
+
* RUNTIME_UNSUPPORTED; node: `sqliteReplicaStorage` exposes it by default).
|
|
776
|
+
*/
|
|
777
|
+
interface ReplicaLifecycleGate {
|
|
778
|
+
readonly kind: "indexeddb" | "file";
|
|
779
|
+
/**
|
|
780
|
+
* Startup recovery before anything binds: browser checks the durable consent deadline and,
|
|
781
|
+
* if expired, denies and crypto-shreds keys before any replica access; it also retries a
|
|
782
|
+
* pending `deleteDatabase` marker and erases intents. Failures remain gated and visible via
|
|
783
|
+
* `pendingPurge()` / `purgePending`; a browser closed at expiry catches it on its next load.
|
|
784
|
+
*/
|
|
785
|
+
recover(o: {
|
|
786
|
+
signal: AbortSignal;
|
|
787
|
+
}): Promise<ReplicaEraseReport>;
|
|
788
|
+
/**
|
|
789
|
+
* Fresh interactive sign-in ONLY. `consentAnchor` comes from independently validated
|
|
790
|
+
* signed owner SIWE with finite `expirationTime`, verified `issuedAt` and an exact-full-
|
|
791
|
+
* space uncaveated KV get+sync witness, or (for a compact-only session) the signed
|
|
792
|
+
* `iat`/`nbf` of the oldest independently verified owner-authorizing proof. A child
|
|
793
|
+
* leaf freshly minted under an old owner grant does not refresh consent; leaf
|
|
794
|
+
* signature/CID/audience alone do not prove it. An invalid/expirationless SIWE
|
|
795
|
+
* session does NOT fall through to a compact leaf or `first_install`. The anchor
|
|
796
|
+
* must be finite ms, clamped to now at capture and strictly newer than the last
|
|
797
|
+
* deny (browser: also the origin tombstone). Missing proof or non-finite anchors
|
|
798
|
+
* cannot establish (`not_established`); network sign-in/restore remain unaffected.
|
|
799
|
+
* Older anchors reject `stamp_stale`. Rejects
|
|
800
|
+
* `logout_pending` / `purge_pending` before mutation. Null principal → set it; matching
|
|
801
|
+
* principal → current stamp; another account → `principal_mismatch`.
|
|
802
|
+
*/
|
|
803
|
+
establishSessionStamp(principal: string, o: {
|
|
804
|
+
consentAnchor: ReplicaConsentAnchor;
|
|
805
|
+
}): Promise<ReplicaSessionStamp>;
|
|
806
|
+
/**
|
|
807
|
+
* Restore ONLY; never mutates the gate. Returns `persisted` when it equals the gate's stamp and
|
|
808
|
+
* principal. Rejects `not_established` (no persisted stamp), `stamp_stale`, `principal_mismatch`,
|
|
809
|
+
* `logout_pending`, `purge_pending`.
|
|
810
|
+
*/
|
|
811
|
+
restoreSessionStamp(principal: string, persisted: ReplicaSessionStamp | undefined): Promise<ReplicaSessionStamp>;
|
|
812
|
+
/** True only for a matching non-null gate principal, with no old intent or marker. A null principal first establishes a fresh session. */
|
|
813
|
+
activationAllowed(next: string): Promise<boolean>;
|
|
814
|
+
/**
|
|
815
|
+
* Synchronous, reason-bearing preflight before open, installGrant, get/list, sync or
|
|
816
|
+
* replica-derived event delivery. An elapsed browser deadline returns `consent_expired`
|
|
817
|
+
* before grant-expiry checks; the controller starts scoped denial and emits one grant
|
|
818
|
+
* deadline event per **opened** handle with a known opaque replicaId. Pre-open denial has
|
|
819
|
+
* no fabricated replica id and uses a lifecycle event/recovery report instead. A timer
|
|
820
|
+
* that already denied returns `gated` (`stamp_stale`). This does not mutate the gate.
|
|
821
|
+
*/
|
|
822
|
+
checkDelivery(stamp: ReplicaSessionStamp): ReplicaDeliveryDecision;
|
|
823
|
+
/**
|
|
824
|
+
* Finite, immutable deadline from the persisted gate for this matching active stamp,
|
|
825
|
+
* synchronously available to the main-thread scheduler even when a restored compact
|
|
826
|
+
* session has no consent anchor. Null if stale, unavailable or not established; never
|
|
827
|
+
* derive a new deadline from `first_install` or the current session's leaf token.
|
|
828
|
+
* Browser timers call `checkDelivery` again before notifying/denying; gate checks on
|
|
829
|
+
* startup/open/read are mandatory independently of whether a timer ran. Node may
|
|
830
|
+
* return null when its gate has no persisted consent deadline.
|
|
831
|
+
*/
|
|
832
|
+
consentDeadline(stamp: ReplicaSessionStamp): number | null;
|
|
833
|
+
/**
|
|
834
|
+
* Synchronous main-thread delivery check before replica-served values/list keys/event fields,
|
|
835
|
+
* and before open, installGrant and sync. Browser checks marker, origin deny tombstone,
|
|
836
|
+
* durable consent deadline against the same injected clock, and this tab's gate view; node
|
|
837
|
+
* checks its process view. No stamp means not deliverable.
|
|
838
|
+
*/
|
|
839
|
+
deliverable(stamp: ReplicaSessionStamp): boolean;
|
|
840
|
+
/**
|
|
841
|
+
* Step 1. Flips the in-memory view before the first await. If the caller principal matches,
|
|
842
|
+
* one commit bumps epoch and deniedAt, records intents, advances pending generations and
|
|
843
|
+
* clears first-install anchors. Browser also deletes account AES-GCM and gate-HMAC keys, plus
|
|
844
|
+
* owner device, in that deny transaction BEFORE background erase; `consent_expired` uses this
|
|
845
|
+
* same path. Node deletes the device through the identity intent (CONTRACT.md §2). A stale
|
|
846
|
+
* principal disables only its own process/tab; the gate is untouched.
|
|
847
|
+
*/
|
|
848
|
+
deny(input: ReplicaDenyInput): Promise<ReplicaDenyOutcome>;
|
|
849
|
+
/** Step 3: erase each outstanding intent in its own exempt transaction. */
|
|
850
|
+
erasePending(o: {
|
|
851
|
+
signal: AbortSignal;
|
|
852
|
+
}): Promise<ReplicaEraseReport>;
|
|
853
|
+
/** Outstanding intents; non-empty means status `purgePending: true`. */
|
|
854
|
+
pendingPurge(): Promise<ReplicaPurgeIntent[]>;
|
|
855
|
+
}
|
|
856
|
+
/** Worker replies that carry replica-served data are stamped; a reply without a stamp is dropped. */
|
|
857
|
+
interface ReplicaStampedReply {
|
|
858
|
+
stamp: ReplicaSessionStamp;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Quota contract (TC-858 Phase 2, CONTRACT.md §4). Types only.
|
|
863
|
+
*
|
|
864
|
+
* Browser: `maxBytes` is origin-wide over the deterministic application encoding of
|
|
865
|
+
* ciphertext entries/blobs/meta and sealed owner-device plus grant/first-install device
|
|
866
|
+
* records. The ledger lives in the `quota` control store; page and device writes admit
|
|
867
|
+
* before any put, then account in the same guarded transaction. Deny releases the reserved
|
|
868
|
+
* device bucket atomically with key deletion; partition erase remains idempotent.
|
|
869
|
+
* Node/CLI: `maxBytes` is per profile (per storage `dir`); there is no ledger — usage is derived
|
|
870
|
+
* from stored content, so erase replay is safe — and the `admit` hook runs inside `applyPage`'s
|
|
871
|
+
* guarded mutation (profile lock held). `minFreeBytes` applies only in the browser.
|
|
872
|
+
*/
|
|
873
|
+
/** Quota settings forwarded from `ReplicationOptions` to storage through `KVReplicaSpec.quota`. */
|
|
874
|
+
interface ReplicaQuotaSpec {
|
|
875
|
+
/** Byte budget; absent means unbounded (Phase 1 behaviour). */
|
|
876
|
+
maxBytes?: number;
|
|
877
|
+
/** Browser free-space floor; the browser adapter defaults it to `DEFAULT_MIN_FREE_BYTES`. */
|
|
878
|
+
minFreeBytes?: number;
|
|
879
|
+
}
|
|
880
|
+
/** Node/CLI page admission keeps its existing blob/entry interpretation. */
|
|
881
|
+
interface ReplicaPageAdmission {
|
|
882
|
+
partitionId: string;
|
|
883
|
+
/** Sum of the page's new blob bytes (node admission hook's existing metric). */
|
|
884
|
+
blobBytes: number;
|
|
885
|
+
/** Number of entry writes; not the browser ciphertext-row accounting metric. */
|
|
886
|
+
entries: number;
|
|
887
|
+
/** Optional caller-supplied free bytes; node's browser-only floor is not applied. */
|
|
888
|
+
freeBytes: number | null;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Browser page admission uses the exact signed net application-encoded byte change of
|
|
892
|
+
* persisted ciphertext entries, blobs and active meta. Erase retains a keyless opaque/numeric
|
|
893
|
+
* purged meta tombstone, charged to the partition until replacement or physical deletion;
|
|
894
|
+
* the same row metric measures it. S5's browser ledger separately combines measured device-row
|
|
895
|
+
* deltas for owner/stamp writes in a reserved device bucket. New rows are sealed before opening
|
|
896
|
+
* the transaction; old stored rows are measured inside it. Replacements/GC may release space.
|
|
897
|
+
* Node's page admission stays unchanged.
|
|
898
|
+
*/
|
|
899
|
+
interface ReplicaBrowserPageAdmission {
|
|
900
|
+
partitionId: string;
|
|
901
|
+
deltaBytes: number;
|
|
902
|
+
/** Free bytes read before opening the transaction; null when unknown. */
|
|
903
|
+
freeBytes: number | null;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Browser ledger seam, called by the adapter in guarded write or exempt erase/deny
|
|
907
|
+
* transactions that include the `quota` store. Every sealed meta mutation (including
|
|
908
|
+
* lease/status) and measured device write uses the origin ledger. Implementations MUST
|
|
909
|
+
* only await requests on `tx`; S5's browser implementation adds combined-row methods
|
|
910
|
+
* without changing this shared public interface.
|
|
911
|
+
*/
|
|
912
|
+
interface ReplicaQuotaLedger<Tx> {
|
|
913
|
+
/** Read outside any transaction, just before it: `navigator.storage.estimate()` quota − usage. */
|
|
914
|
+
freeBytes(): Promise<number | null>;
|
|
915
|
+
/**
|
|
916
|
+
* Admits the exact projected application-encoded persisted ciphertext-row delta, including
|
|
917
|
+
* sealed value/IV/tag plus persisted key/index fields and envelope framing. IDB engine/page
|
|
918
|
+
* overhead is unobservable and excluded. The adapter uses the same measured delta for
|
|
919
|
+
* `accountCommit`; never admits plaintext lengths or a per-entry estimate. On
|
|
920
|
+
* STORAGE_BUDGET it aborts `tx` before any page write.
|
|
921
|
+
*/
|
|
922
|
+
admitPage(tx: Tx, page: ReplicaBrowserPageAdmission): Promise<void>;
|
|
923
|
+
/** Records the same measured signed delta in the page/repair/GC/meta transaction. */
|
|
924
|
+
accountCommit(tx: Tx, o: {
|
|
925
|
+
partitionId: string;
|
|
926
|
+
deltaBytes: number;
|
|
927
|
+
}): Promise<void>;
|
|
928
|
+
/** Subtracts the exact encoded bytes removed in the erase transaction, idempotently. */
|
|
929
|
+
accountErase(tx: Tx, o: {
|
|
930
|
+
partitionId: string;
|
|
931
|
+
bytes: number;
|
|
932
|
+
}): Promise<void>;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Node/CLI admission hook (`admit` on the SQLite store options), run inside `applyPage`'s guarded
|
|
936
|
+
* mutation before any write. Rejects with code STORAGE_BUDGET to refuse the page.
|
|
937
|
+
*/
|
|
938
|
+
type ReplicaAdmitHook = (page: ReplicaPageAdmission) => void | Promise<void>;
|
|
939
|
+
|
|
940
|
+
interface ReplicationIdentity {
|
|
941
|
+
/** Canonical HTTP(S) URL: origin + path without trailing slash; no credentials/query/fragment. */
|
|
942
|
+
host: string;
|
|
943
|
+
/** Full TinyCloud space id; only the EIP-155 address segment is lower-cased. */
|
|
944
|
+
space: string;
|
|
945
|
+
/** Principal `did:pkh:eip155:…`; only the EIP-155 address segment is lower-cased. */
|
|
946
|
+
principal: string;
|
|
947
|
+
}
|
|
948
|
+
type ReplicaCoverage = "empty" | "bootstrapping" | "complete";
|
|
949
|
+
type ReplicaAuthorityState = "valid" | "expired" | "revoked" | "not-yet-valid";
|
|
950
|
+
interface LocalReadMeta {
|
|
951
|
+
asOf: string | null;
|
|
952
|
+
coverage: ReplicaCoverage;
|
|
953
|
+
authority: ReplicaAuthorityState;
|
|
954
|
+
/** Durable identity commit epoch captured before the last successful sync began. */
|
|
955
|
+
syncedThroughEpoch: number;
|
|
956
|
+
}
|
|
957
|
+
type LocalGetResult = {
|
|
958
|
+
status: "present";
|
|
959
|
+
key: string;
|
|
960
|
+
value: Uint8Array;
|
|
961
|
+
etag: string;
|
|
962
|
+
metadata: Record<string, string>;
|
|
963
|
+
meta: LocalReadMeta;
|
|
964
|
+
} | {
|
|
965
|
+
status: "content_missing";
|
|
966
|
+
key: string;
|
|
967
|
+
etag: string;
|
|
968
|
+
metadata: Record<string, string>;
|
|
969
|
+
meta: LocalReadMeta;
|
|
970
|
+
} | {
|
|
971
|
+
status: "deleted" | "absent" | "coverage_incomplete" | "not_covered";
|
|
972
|
+
key: string;
|
|
973
|
+
meta: LocalReadMeta;
|
|
974
|
+
};
|
|
975
|
+
/** Live keys under a plain `startsWith` prefix, after-exclusive UTF-8 byte ordering, at most limit. */
|
|
976
|
+
interface LocalListResult {
|
|
977
|
+
keys: string[];
|
|
978
|
+
meta: LocalReadMeta;
|
|
979
|
+
}
|
|
980
|
+
type LocalSyncResult = {
|
|
981
|
+
status: "synced";
|
|
982
|
+
pages: number;
|
|
983
|
+
changes: number;
|
|
984
|
+
deleted: number;
|
|
985
|
+
fetched: number;
|
|
986
|
+
contentMissing: number;
|
|
987
|
+
coverage: ReplicaCoverage;
|
|
988
|
+
/** The input syncStartEpoch, persisted atomically only when this sync succeeds. */
|
|
989
|
+
syncedThroughEpoch: number;
|
|
990
|
+
} | {
|
|
991
|
+
status: "busy";
|
|
992
|
+
};
|
|
993
|
+
interface ReplicaGrantInfo {
|
|
994
|
+
cid: string;
|
|
995
|
+
parentCid: string | null;
|
|
996
|
+
/** The signed `exp`, ms since epoch; null when the grant has none. */
|
|
997
|
+
expiresAt: number | null;
|
|
998
|
+
/** Grant lifecycle: pending until confirmed/activated by a successful replica sync. */
|
|
999
|
+
state: "active" | "pending";
|
|
1000
|
+
/** Both get and sync in the grant carry no caveats. */
|
|
1001
|
+
unconstrained: boolean;
|
|
1002
|
+
/**
|
|
1003
|
+
* ms since epoch, from the storage clock at the FIRST install of this grant CID, stored keyed
|
|
1004
|
+
* by CID and set once. Re-installing the same CID never refreshes it; recompute-at-open uses the
|
|
1005
|
+
* stored value.
|
|
1006
|
+
*/
|
|
1007
|
+
installedAt?: number;
|
|
1008
|
+
/**
|
|
1009
|
+
* min(expiresAt, installedAt + grantTtlMs, consentDeadline), stored with the grant and
|
|
1010
|
+
* recomputed at open from the stored `installedAt` and the handle's `spec.grantPolicy`. Serving
|
|
1011
|
+
* requires `now < effectiveExpiresAt` with a finite stored value. Otherwise (NaN, ±Infinity,
|
|
1012
|
+
* missing) the effective authority is not `valid`, and get/list throw GRANT_EXPIRED inside the
|
|
1013
|
+
* read transaction without returning bytes or keys. Present on every Phase 2 grant.
|
|
1014
|
+
*/
|
|
1015
|
+
effectiveExpiresAt?: number;
|
|
1016
|
+
/**
|
|
1017
|
+
* ms since epoch; consent anchor + CONSENT_WINDOW_MS. Serving and minting require
|
|
1018
|
+
* `now < consentDeadline` and a finite value (refusal code CONSENT_EXPIRED).
|
|
1019
|
+
*/
|
|
1020
|
+
consentDeadline?: number;
|
|
1021
|
+
/**
|
|
1022
|
+
* The resolved consent anchor (finite ms, clamped to ≤ now). A `first_install` anchor is keyed by
|
|
1023
|
+
* identity (node: gate file `firstInstall`; browser: `device` store), survives partition reset,
|
|
1024
|
+
* purge, revocation and reinstall, and is cleared only by `deny`. An unreadable anchor fails
|
|
1025
|
+
* closed (no serving, no mint).
|
|
1026
|
+
*/
|
|
1027
|
+
consentAnchor?: ReplicaConsentAnchor;
|
|
1028
|
+
}
|
|
1029
|
+
interface LocalReplicaStatus {
|
|
1030
|
+
coverage: ReplicaCoverage;
|
|
1031
|
+
lastSyncAt: string | null;
|
|
1032
|
+
/** Durable identity commit epoch captured before the last successful sync began. */
|
|
1033
|
+
syncedThroughEpoch: number;
|
|
1034
|
+
authority: {
|
|
1035
|
+
state: ReplicaAuthorityState;
|
|
1036
|
+
expiresAt: string | null;
|
|
1037
|
+
};
|
|
1038
|
+
grant: ReplicaGrantInfo | null;
|
|
1039
|
+
counts: {
|
|
1040
|
+
keys: number;
|
|
1041
|
+
contentMissing: number;
|
|
1042
|
+
tombstones: number;
|
|
1043
|
+
};
|
|
1044
|
+
bytes: number;
|
|
1045
|
+
lastError: {
|
|
1046
|
+
at: string;
|
|
1047
|
+
code: string;
|
|
1048
|
+
message: string;
|
|
1049
|
+
} | null;
|
|
1050
|
+
/**
|
|
1051
|
+
* Browser: result of `navigator.storage.persist()`; null when the API is missing or not yet
|
|
1052
|
+
* asked. Node/CLI omit it (disk storage is persistent).
|
|
1053
|
+
*/
|
|
1054
|
+
persisted?: boolean | null;
|
|
1055
|
+
/** A purge intent covers this partition (or its identity); it is unreadable until erased. */
|
|
1056
|
+
purgePending?: boolean;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Ungated browser inspection returns only opaque, unsealed numeric controls from the
|
|
1060
|
+
* v2 meta row: coverageCode, syncedThroughEpoch, keys/contentMissing/tombstones and
|
|
1061
|
+
* contentBytes. It must not acquire the account key or decrypt grant, authority, cursor,
|
|
1062
|
+
* errors or KV payload. `redacted: true` distinguishes unknown from absent/revoked.
|
|
1063
|
+
*/
|
|
1064
|
+
interface ReplicaInspectedStatus {
|
|
1065
|
+
redacted: true;
|
|
1066
|
+
coverage: ReplicaCoverage;
|
|
1067
|
+
syncedThroughEpoch: number;
|
|
1068
|
+
counts: LocalReplicaStatus["counts"];
|
|
1069
|
+
bytes: number;
|
|
1070
|
+
persisted?: boolean | null;
|
|
1071
|
+
purgePending?: boolean;
|
|
1072
|
+
}
|
|
1073
|
+
interface ReplicaDevice {
|
|
1074
|
+
did: string;
|
|
1075
|
+
jwk: object;
|
|
1076
|
+
}
|
|
1077
|
+
/** Identity is canonical; `space` is the session's verbatim space id for replica calls. */
|
|
1078
|
+
interface KVReplicaSpec {
|
|
1079
|
+
identity: ReplicationIdentity;
|
|
1080
|
+
space: string;
|
|
1081
|
+
prefix: string;
|
|
1082
|
+
allowSecrets: boolean;
|
|
1083
|
+
/**
|
|
1084
|
+
* When present, adapters MUST use this device for partition naming, grant audience checks,
|
|
1085
|
+
* and signing sync requests; they MUST NOT load or create their own device key for this handle.
|
|
1086
|
+
*/
|
|
1087
|
+
device?: ReplicaDevice;
|
|
1088
|
+
/** Graph-scoped fetch for main-thread adapters; worker adapters ignore it. */
|
|
1089
|
+
fetch?: FetchFunction;
|
|
1090
|
+
/**
|
|
1091
|
+
* The session stamp this handle is bound to; immutable for the handle's life. With a `lifecycle`
|
|
1092
|
+
* gate (mandatory when replication is on), a spec without it is refused REPLICA_GATED
|
|
1093
|
+
* `not_established`, and every guarded transaction runs through `ReplicaGateTx.run` with it.
|
|
1094
|
+
* Use {@link ReplicaGatedSpec} where the stamp must be present.
|
|
1095
|
+
*/
|
|
1096
|
+
stamp?: ReplicaSessionStamp;
|
|
1097
|
+
/**
|
|
1098
|
+
* Grant lifetime policy. Mandatory when replication is on: the controller always sets it, and a
|
|
1099
|
+
* Phase 2 adapter rejects `open` without it (REPLICA_INVALID_ARGUMENT). Optional in the type only
|
|
1100
|
+
* so Phase 1 callers compile.
|
|
1101
|
+
*/
|
|
1102
|
+
grantPolicy?: ReplicaGrantPolicy;
|
|
1103
|
+
/** Quota settings; absent means unbounded. */
|
|
1104
|
+
quota?: ReplicaQuotaSpec;
|
|
1105
|
+
/**
|
|
1106
|
+
* Called when another tab/process commits a sync for this partition (browser: the worker's
|
|
1107
|
+
* `committed` BroadcastChannel message). The controller emits `replication.sync` with
|
|
1108
|
+
* `trigger: "peer"`, `origin: "peer"`. Must not throw; must not be called for own commits.
|
|
1109
|
+
*/
|
|
1110
|
+
onPeerCommit?(info: ReplicaPeerCommit): void;
|
|
1111
|
+
}
|
|
1112
|
+
interface ReplicaPeerCommit {
|
|
1113
|
+
/** ms since epoch of the peer's commit. */
|
|
1114
|
+
at: number;
|
|
1115
|
+
syncedThroughEpoch: number;
|
|
1116
|
+
changes?: number;
|
|
1117
|
+
coverage?: ReplicaCoverage;
|
|
1118
|
+
}
|
|
1119
|
+
/** A spec as a gated adapter accepts it: stamp and grant policy present. */
|
|
1120
|
+
type ReplicaGatedSpec = KVReplicaSpec & {
|
|
1121
|
+
stamp: ReplicaSessionStamp;
|
|
1122
|
+
grantPolicy: ReplicaGrantPolicy;
|
|
1123
|
+
};
|
|
1124
|
+
interface KVReplicaHandle {
|
|
1125
|
+
readonly spec: KVReplicaSpec;
|
|
1126
|
+
/** Always equals `spec.device.did` when an explicit device was provided. */
|
|
1127
|
+
readonly deviceDid: string;
|
|
1128
|
+
/**
|
|
1129
|
+
* Opaque partition id for events. Required on Phase 2 `ReplicaGatedHandle`; optional here
|
|
1130
|
+
* only for Phase 1 adapters. Browser: private gate-HMAC partition id; node: replica hash.
|
|
1131
|
+
* A controller MUST NOT derive one from raw prefix/space or emit a grant event without it.
|
|
1132
|
+
*/
|
|
1133
|
+
readonly replicaId?: string;
|
|
1134
|
+
/**
|
|
1135
|
+
* Guarded read. Adapters MUST NOT return value bytes unless the effective authority is `valid`
|
|
1136
|
+
* (signed exp, effective expiry, consent deadline, node-attested authority, revocation): they
|
|
1137
|
+
* throw GRANT_EXPIRED / GRANT_REVOKED / GRANT_NOT_YET_VALID / CONSENT_EXPIRED inside the read
|
|
1138
|
+
* transaction, and the controller falls through to the network with the matching reason. An
|
|
1139
|
+
* erased partition rejects RESET_REQUIRED (by tombstone/generation, not by unlink).
|
|
1140
|
+
*/
|
|
1141
|
+
get(key: string): Promise<LocalGetResult>;
|
|
1142
|
+
/** Guarded read; same authority rule as `get` (no keys unless authority is `valid`). */
|
|
1143
|
+
list(o: {
|
|
1144
|
+
prefix: string;
|
|
1145
|
+
after?: string;
|
|
1146
|
+
limit?: number;
|
|
1147
|
+
}): Promise<LocalListResult>;
|
|
1148
|
+
/** Guarded read (runs through `ReplicaGateTx.run`). */
|
|
1149
|
+
grant(): Promise<ReplicaGrantInfo | null>;
|
|
1150
|
+
/**
|
|
1151
|
+
* Installs a grant after validating audience (the handle's device), `kv/get` + `kv/sync` coverage
|
|
1152
|
+
* of the prefix, and no caveats (caveated grants reject GRANT_NOT_COVERING). Stores `installedAt`
|
|
1153
|
+
* on first install of the CID and `effectiveExpiresAt`. Guarded: REPLICA_GATED.
|
|
1154
|
+
*/
|
|
1155
|
+
installGrant(ucan: string): Promise<ReplicaGrantInfo>;
|
|
1156
|
+
/**
|
|
1157
|
+
* The controller supplies syncStartEpoch, captured from this identity's committedEpoch before
|
|
1158
|
+
* starting the sync. Every adapter, including the browser worker, MUST persist that exact epoch
|
|
1159
|
+
* in the same transaction as the sync's completion marker, only after the whole sync finishes
|
|
1160
|
+
* (feed pages, repairs and GC). Failed, aborted and busy syncs, including a failure after the
|
|
1161
|
+
* last feed page, MUST leave the prior fence unchanged. A successful result returns that
|
|
1162
|
+
* persisted value as syncedThroughEpoch. Aborting `signal` stops at the last complete page;
|
|
1163
|
+
* nothing past it commits. Gated: REPLICA_GATED; quota refusal: STORAGE_BUDGET / STORAGE_FULL,
|
|
1164
|
+
* keeping committed content.
|
|
1165
|
+
*/
|
|
1166
|
+
sync(o: {
|
|
1167
|
+
signal: AbortSignal;
|
|
1168
|
+
syncStartEpoch: number;
|
|
1169
|
+
}): Promise<LocalSyncResult>;
|
|
1170
|
+
/** Status reports the durable syncedThroughEpoch, including after reopening this handle. */
|
|
1171
|
+
status(): Promise<LocalReplicaStatus>;
|
|
1172
|
+
/** Cancels and awaits in-flight syncs before closing. */
|
|
1173
|
+
close(): Promise<void>;
|
|
1174
|
+
}
|
|
1175
|
+
/** Phase 2 opened handle: immutable stamp/policy and an opaque event identifier. */
|
|
1176
|
+
type ReplicaGatedHandle = KVReplicaHandle & {
|
|
1177
|
+
readonly spec: ReplicaGatedSpec;
|
|
1178
|
+
readonly replicaId: string;
|
|
1179
|
+
};
|
|
1180
|
+
interface PurgeTarget {
|
|
1181
|
+
identity: ReplicationIdentity;
|
|
1182
|
+
/** Verbatim session space id used by the corresponding replica handle. */
|
|
1183
|
+
space: string;
|
|
1184
|
+
prefix: string;
|
|
1185
|
+
/** Session device in delegate posture, when one is used. */
|
|
1186
|
+
sessionDeviceDid?: string;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Replica persistence adapters. B-node's Node/CLI adapter MUST provide durable pending state.
|
|
1190
|
+
* If that state cannot be opened (for example, a file-store failure), it MUST surface the
|
|
1191
|
+
* unavailable/error to the controller; the controller MUST fail closed to network reads and
|
|
1192
|
+
* MUST NOT silently substitute memory.
|
|
1193
|
+
*/
|
|
1194
|
+
interface KVReplicaStorage {
|
|
1195
|
+
readonly kind: "sqlite" | "indexeddb";
|
|
1196
|
+
open(spec: KVReplicaSpec): Promise<KVReplicaHandle>;
|
|
1197
|
+
/**
|
|
1198
|
+
* Read an existing replica's durable status without creating/mutating storage or waiting for a
|
|
1199
|
+
* mutation guard. Missing replicas return undefined; abort is honored. Ungated (no stamp):
|
|
1200
|
+
* browser returns a discriminated, redacted control-only status without acquiring the account
|
|
1201
|
+
* key or decrypting data; node/CLI may expose existing grant CIDs/times. Known metadata exposure.
|
|
1202
|
+
*/
|
|
1203
|
+
inspectStatus?(spec: KVReplicaSpec, options: {
|
|
1204
|
+
signal: AbortSignal;
|
|
1205
|
+
}): Promise<LocalReplicaStatus | ReplicaInspectedStatus | undefined>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Erases replicas for the identity and prefix across the deduplicated union of the adapter's
|
|
1208
|
+
* stored device (if any) and `sessionDeviceDid`. A missing stored device never skips the
|
|
1209
|
+
* session-device purge. Does not open, mint, or sync. Erasure removes entries, blobs, grant,
|
|
1210
|
+
* config, and authority; earlier handles reject with RESET_REQUIRED. A contentless tombstone
|
|
1211
|
+
* may remain. Rejects with an error carrying `code` on failure.
|
|
1212
|
+
*/
|
|
1213
|
+
purge(target: PurgeTarget): Promise<void>;
|
|
1214
|
+
/**
|
|
1215
|
+
* Optional only for volatile runtimes. Omission means an in-memory store with `durable: false`.
|
|
1216
|
+
* B-node's Node/CLI adapter MUST provide a durable store and surface open failures, never
|
|
1217
|
+
* silently fall back to memory.
|
|
1218
|
+
*
|
|
1219
|
+
* With a `lifecycle` gate, the store is bound to `options.stamp` for its whole life (callers
|
|
1220
|
+
* cache stores per identity AND stamp). Every `read`, `readExisting` and `update` (append,
|
|
1221
|
+
* settle, remove, clear, flush) checks stamp, principal, incarnation and identity intent
|
|
1222
|
+
* (plus browser keyId) before touching storage; missing stamps refuse REPLICA_GATED
|
|
1223
|
+
* `not_established`. Browser derives private gate-HMAC ids asynchronously from `snapshot()`
|
|
1224
|
+
* before an IDB transaction, locks on that opaque identity id, then calls main-thread
|
|
1225
|
+
* `ReplicaGateTx.run`, whose first control request verifies the snapshot/keyId. Node's
|
|
1226
|
+
* identity-scoped `pending.json` is not SQLite: both reads and mutations take the mandatory
|
|
1227
|
+
* profile lock across gate/generation check and file access; mutations durably rename
|
|
1228
|
+
* before releasing it.
|
|
1229
|
+
* The identity generation advances atomically with a deny intent, so stale stores cannot
|
|
1230
|
+
* refill erased pending writes (CONTRACT.md §2). Missing locks fail closed.
|
|
1231
|
+
*/
|
|
1232
|
+
pendingWrites?(identity: ReplicationIdentity, options?: {
|
|
1233
|
+
stamp?: ReplicaSessionStamp;
|
|
1234
|
+
}): PendingWriteStore;
|
|
1235
|
+
/**
|
|
1236
|
+
* The platform lifecycle gate (browser: origin DB control stores + logout marker; node/CLI:
|
|
1237
|
+
* gate file). Mandatory when replication is on: `indexedDbReplicaStorage()` and
|
|
1238
|
+
* `sqliteReplicaStorage()` expose it (node's `gateFile` defaults next to `dir`), and the web
|
|
1239
|
+
* runtime refuses storage without it with RUNTIME_UNSUPPORTED. Optional in the type only so
|
|
1240
|
+
* Phase 1 test doubles compile; use {@link ReplicaGatedStorage} for Phase 2 adapters.
|
|
1241
|
+
*/
|
|
1242
|
+
readonly lifecycle?: ReplicaLifecycleGate;
|
|
1243
|
+
}
|
|
1244
|
+
/** A Phase 2 adapter: the unstamped Phase 1 `open` overload is deliberately absent. */
|
|
1245
|
+
type ReplicaGatedStorage = Omit<KVReplicaStorage, "open" | "lifecycle"> & {
|
|
1246
|
+
readonly lifecycle: ReplicaLifecycleGate;
|
|
1247
|
+
open(spec: ReplicaGatedSpec): Promise<ReplicaGatedHandle>;
|
|
1248
|
+
};
|
|
1249
|
+
interface PendingWriteRecord {
|
|
1250
|
+
opId: string;
|
|
1251
|
+
seq: number;
|
|
1252
|
+
key: string;
|
|
1253
|
+
op: "put" | "delete";
|
|
1254
|
+
state: "in_flight" | "committed" | "ambiguous";
|
|
1255
|
+
epoch: number | null;
|
|
1256
|
+
at: string;
|
|
1257
|
+
settledAt: string | null;
|
|
1258
|
+
code?: string;
|
|
1259
|
+
}
|
|
1260
|
+
/** One identity-scoped pending state, with a monotonically increasing commit epoch and sequence. */
|
|
1261
|
+
interface PendingWriteState {
|
|
1262
|
+
v: 2;
|
|
1263
|
+
identity: ReplicationIdentity;
|
|
1264
|
+
committedEpoch: number;
|
|
1265
|
+
seq: number;
|
|
1266
|
+
records: PendingWriteRecord[];
|
|
1267
|
+
}
|
|
1268
|
+
/** Identity-scoped pending state and its restart-durability guarantee. */
|
|
1269
|
+
interface PendingWriteStore {
|
|
1270
|
+
/**
|
|
1271
|
+
* True only when committedEpoch and all records survive process restarts; false for memory or
|
|
1272
|
+
* other volatile stores.
|
|
1273
|
+
*/
|
|
1274
|
+
readonly durable: boolean;
|
|
1275
|
+
readonly identity: ReplicationIdentity;
|
|
1276
|
+
/**
|
|
1277
|
+
* Consistent gated snapshot. Browser: guarded IDB read; node/CLI: mandatory profile lock
|
|
1278
|
+
* across gate check and pending.json read (not an unlocked atomic-file snapshot).
|
|
1279
|
+
*/
|
|
1280
|
+
read(): Promise<PendingWriteState>;
|
|
1281
|
+
/**
|
|
1282
|
+
* Read an existing durable snapshot without initializing storage; undefined means no file exists.
|
|
1283
|
+
* Gated.
|
|
1284
|
+
*/
|
|
1285
|
+
readExisting?(): Promise<PendingWriteState | undefined>;
|
|
1286
|
+
/**
|
|
1287
|
+
* Serialized read-modify-write; durable stores persist before resolve. Errors on mismatch/failure.
|
|
1288
|
+
* Gated: the assert runs inside the same transaction/guard as the write.
|
|
1289
|
+
*/
|
|
1290
|
+
update<T>(mutate: (s: PendingWriteState) => T): Promise<T>;
|
|
1291
|
+
}
|
|
1292
|
+
type AuthorityRefusal = {
|
|
1293
|
+
refused: "NOT_COVERED" | "CAVEATED_AUTHORITY" | "SESSION_EXPIRING";
|
|
1294
|
+
};
|
|
1295
|
+
interface ReplicationAuthority {
|
|
1296
|
+
/**
|
|
1297
|
+
* Compact delegated/session-only authority: never consult `plan`, mint or silently renew.
|
|
1298
|
+
* A matching persisted stamp and independently verified, active installed grant are not
|
|
1299
|
+
* enough to serve locally: the CURRENT session must also have uncaveated get authority
|
|
1300
|
+
* for each requested key/list prefix. If `sessionGrant(prefix)` refuses NOT_COVERED,
|
|
1301
|
+
* CAVEATED_AUTHORITY or SESSION_EXPIRING, use network KV even while that stored grant
|
|
1302
|
+
* remains active. A stored grant never establishes fresh consent or replaces the
|
|
1303
|
+
* current session's own authority.
|
|
1304
|
+
*/
|
|
1305
|
+
sessionOnly?: boolean;
|
|
1306
|
+
sessionGrant(prefix: string): {
|
|
1307
|
+
ucan: string;
|
|
1308
|
+
device: ReplicaDevice;
|
|
1309
|
+
} | AuthorityRefusal;
|
|
1310
|
+
plan(prefix: string): {
|
|
1311
|
+
path: "session" | "runtime";
|
|
1312
|
+
parentCid: string;
|
|
1313
|
+
expiresAt: number;
|
|
1314
|
+
} | AuthorityRefusal;
|
|
1315
|
+
mint(deviceDid: string, prefix: string, signal: AbortSignal,
|
|
1316
|
+
/**
|
|
1317
|
+
* `expiryMs` is a DURATION from now (forwarded as `delegateTo(…, { expiry: expiryMs })`), from
|
|
1318
|
+
* `mintRemainingMs`. Absent means Phase 1 behaviour (session-tier default).
|
|
1319
|
+
*/
|
|
1320
|
+
options?: {
|
|
1321
|
+
expiryMs?: number;
|
|
1322
|
+
}): Promise<{
|
|
1323
|
+
ucan: string;
|
|
1324
|
+
parentCid: string;
|
|
1325
|
+
expiresAt: number;
|
|
1326
|
+
}>;
|
|
1327
|
+
/**
|
|
1328
|
+
* Finite consent anchor from an independently validated signed owner SIWE `issuedAt`
|
|
1329
|
+
* with finite signed `expirationTime` and an exact-full-space verified, uncaveated KV
|
|
1330
|
+
* get+sync witness, or the independently verified **oldest owner-authorizing** compact
|
|
1331
|
+
* proof's signed `iat`/`nbf` (never its child leaf). Null when owner proof bytes,
|
|
1332
|
+
* KV witness, finite SIWE expiration or proof timestamp are unavailable: arbitrary
|
|
1333
|
+
* did:key/empty-prf leaves and compact leaf CID/prf alone are insufficient even with
|
|
1334
|
+
* finite leaf `iat`/`nbf`. Storage may use `first_install` only to bound expiry;
|
|
1335
|
+
* null cannot establish a fresh session stamp.
|
|
1336
|
+
*/
|
|
1337
|
+
consentAnchor?(): ReplicaConsentAnchor | null;
|
|
1338
|
+
/** True only for verified owner session-key derivation. Compact delegated / sessionOnly cannot silently renew. */
|
|
1339
|
+
readonly canRenew?: boolean;
|
|
1340
|
+
}
|
|
1341
|
+
interface ReplicationScheduler {
|
|
1342
|
+
now(): number;
|
|
1343
|
+
setTimeout(fn: () => void, ms: number): () => void;
|
|
1344
|
+
}
|
|
1345
|
+
interface KVReplicationDeps {
|
|
1346
|
+
options: ResolvedReplicationOptions;
|
|
1347
|
+
mode: "background" | "foreground";
|
|
1348
|
+
storage: KVReplicaStorage;
|
|
1349
|
+
identity: ReplicationIdentity;
|
|
1350
|
+
session: {
|
|
1351
|
+
id: string;
|
|
1352
|
+
did: string;
|
|
1353
|
+
space: string;
|
|
1354
|
+
};
|
|
1355
|
+
authority: ReplicationAuthority;
|
|
1356
|
+
/**
|
|
1357
|
+
* The controller MUST enforce the catch-up trust invariant. With durable pending state,
|
|
1358
|
+
* committedEpoch and records survive restarts, so the syncedThroughEpoch fence applies normally.
|
|
1359
|
+
* With non-durable state, a replica may serve local reads only after a successful sync that
|
|
1360
|
+
* started in this process after this store was created. Until then, every read MUST use the
|
|
1361
|
+
* network with REPLICA_UNPROVEN_SINCE_START; an older durable syncedThroughEpoch is not proof
|
|
1362
|
+
* after restart. Pending-state failures also fail closed to network reads.
|
|
1363
|
+
*/
|
|
1364
|
+
pending: PendingWriteStore;
|
|
1365
|
+
/** Node `/info` features discovered on sign-in for protocol gating. */
|
|
1366
|
+
nodeFeatures?: readonly string[];
|
|
1367
|
+
/**
|
|
1368
|
+
* This session's stamp; forwarded into every `KVReplicaSpec.stamp` and pending store. With
|
|
1369
|
+
* `storage.lifecycle` present, a missing stamp makes the controller `gated` (`not_established`)
|
|
1370
|
+
* and it never opens. A controller is never rebound with a new stamp for an existing session
|
|
1371
|
+
* object: a new stamp requires a new session.
|
|
1372
|
+
*/
|
|
1373
|
+
stamp?: ReplicaSessionStamp;
|
|
1374
|
+
scheduler: ReplicationScheduler;
|
|
1375
|
+
emit(event: ReplicationEvent): void;
|
|
1376
|
+
fetch?: FetchFunction;
|
|
1377
|
+
previous?: Promise<void>;
|
|
1378
|
+
}
|
|
1379
|
+
interface KVListPage {
|
|
1380
|
+
keys: string[];
|
|
1381
|
+
truncated?: boolean;
|
|
1382
|
+
nextCursor?: string;
|
|
1383
|
+
}
|
|
1384
|
+
interface KVReadThrough {
|
|
1385
|
+
/** The callback executes the complete existing network implementation under the caller's signal. */
|
|
1386
|
+
get<T>(r: {
|
|
1387
|
+
space: string;
|
|
1388
|
+
key: string;
|
|
1389
|
+
path: string;
|
|
1390
|
+
options: KVGetOptions | undefined;
|
|
1391
|
+
signal: AbortSignal;
|
|
1392
|
+
network: () => Promise<Result<KVResponse<T>>>;
|
|
1393
|
+
}): Promise<Result<KVResponse<T>>>;
|
|
1394
|
+
list(r: {
|
|
1395
|
+
space: string;
|
|
1396
|
+
listPath: string;
|
|
1397
|
+
options: KVListOptions | undefined;
|
|
1398
|
+
signal: AbortSignal;
|
|
1399
|
+
network: () => Promise<Result<KVListPage>>;
|
|
1400
|
+
}): Promise<Result<KVListPage>>;
|
|
1401
|
+
write<T>(r: {
|
|
1402
|
+
op: "put" | "delete" | "batchPut";
|
|
1403
|
+
space: string;
|
|
1404
|
+
entries: ReadonlyArray<{
|
|
1405
|
+
path: string;
|
|
1406
|
+
body?: Blob | string;
|
|
1407
|
+
}>;
|
|
1408
|
+
signal: AbortSignal;
|
|
1409
|
+
conditional?: boolean;
|
|
1410
|
+
network: () => Promise<Result<T>>;
|
|
1411
|
+
}): Promise<Result<T>>;
|
|
1412
|
+
/**
|
|
1413
|
+
* Best-effort observation for a caller-forced network read, invoked after it settles. This
|
|
1414
|
+
* synchronous event hook MUST NOT read replica storage or start a sync. KVService catches and
|
|
1415
|
+
* ignores thrown errors so the observation cannot change the network read.
|
|
1416
|
+
*/
|
|
1417
|
+
observeNetworkRequested(r: {
|
|
1418
|
+
op: "get" | "list";
|
|
1419
|
+
space: string;
|
|
1420
|
+
/** Absolute get path, or the fully resolved list path. */
|
|
1421
|
+
path: string;
|
|
1422
|
+
reason: "NETWORK_REQUESTED";
|
|
1423
|
+
outcome: "found" | "not_found" | "error";
|
|
1424
|
+
latencyMs: number;
|
|
1425
|
+
}): void;
|
|
1426
|
+
}
|
|
1427
|
+
interface KVReplicationController extends KVReadThrough {
|
|
1428
|
+
status(): Promise<ReplicaStatusEntry[]>;
|
|
1429
|
+
sync(o?: {
|
|
1430
|
+
prefix?: string;
|
|
1431
|
+
signal?: AbortSignal;
|
|
1432
|
+
}): Promise<void>;
|
|
1433
|
+
/** Not async: synchronously stops timers/syncs and blocks new local opens before returning its promise. */
|
|
1434
|
+
purge(o?: {
|
|
1435
|
+
timeoutMs?: number;
|
|
1436
|
+
}): Promise<ReplicationPurgeReport>;
|
|
1437
|
+
clearPending(): Promise<number>;
|
|
1438
|
+
/** Synchronously aborts syncs; resolves after settlement and handle closure. */
|
|
1439
|
+
close(): Promise<void>;
|
|
1440
|
+
}
|
|
1441
|
+
interface ReplicationControl {
|
|
1442
|
+
status(): Promise<ReplicaStatusEntry[]>;
|
|
1443
|
+
sync(o?: {
|
|
1444
|
+
prefix?: string;
|
|
1445
|
+
}): Promise<void>;
|
|
1446
|
+
/**
|
|
1447
|
+
* Synchronous stop phase; promise never rejects. Awaits aborted syncs (bounded by timeout),
|
|
1448
|
+
* closes handles, purges every configured prefix of the bound/last identity, and clears only
|
|
1449
|
+
* records beneath successfully purged prefixes. Failures are returned and emitted.
|
|
1450
|
+
*/
|
|
1451
|
+
purge(o?: {
|
|
1452
|
+
timeoutMs?: number;
|
|
1453
|
+
}): Promise<ReplicationPurgeReport>;
|
|
1454
|
+
/**
|
|
1455
|
+
* Removes all ambiguous records and in-flight records older than ten minutes; younger in-flight
|
|
1456
|
+
* and committed records remain. Exposed callers MUST show CLEAR_PENDING_WARNING first.
|
|
1457
|
+
*/
|
|
1458
|
+
clearPending(): Promise<number>;
|
|
1459
|
+
close(): Promise<void>;
|
|
1460
|
+
}
|
|
1461
|
+
interface ReplicationOptions {
|
|
1462
|
+
enabled: boolean;
|
|
1463
|
+
/** Non-empty list of folder prefixes ending in `/`, or `/` for the whole space. */
|
|
1464
|
+
prefixes: string[];
|
|
1465
|
+
/** Required for prefixes/spaces where requiresSecretsOptIn returns true. */
|
|
1466
|
+
allowSecrets?: boolean;
|
|
1467
|
+
/** Background interval; default 60 seconds. */
|
|
1468
|
+
syncIntervalMs?: number;
|
|
1469
|
+
maxStalenessMs?: number;
|
|
1470
|
+
staleSyncTimeoutMs?: number;
|
|
1471
|
+
verify?: boolean;
|
|
1472
|
+
onEvent?: (event: ReplicationEvent) => void;
|
|
1473
|
+
/**
|
|
1474
|
+
* Device-grant lifetime from install; default DEFAULT_GRANT_TTL_MS (24 h). Values ≤ 0 or above
|
|
1475
|
+
* MAX_GRANT_TTL_MS (7 d) are a TypeError at construction.
|
|
1476
|
+
*/
|
|
1477
|
+
grantTtlMs?: number;
|
|
1478
|
+
/**
|
|
1479
|
+
* Replica byte budget; absent means unbounded. Browser: origin-wide across every partition.
|
|
1480
|
+
* Node/CLI: per profile (per storage dir). Forwarded to storage via `KVReplicaSpec.quota`.
|
|
1481
|
+
*/
|
|
1482
|
+
maxBytes?: number;
|
|
1483
|
+
/** Browser free-space floor; default DEFAULT_MIN_FREE_BYTES (64 MiB). Ignored on node/CLI. */
|
|
1484
|
+
minFreeBytes?: number;
|
|
1485
|
+
}
|
|
1486
|
+
interface ResolvedReplicationOptions extends ReplicationOptions {
|
|
1487
|
+
syncIntervalMs: number;
|
|
1488
|
+
maxStalenessMs: number;
|
|
1489
|
+
staleSyncTimeoutMs: number;
|
|
1490
|
+
allowSecrets: boolean;
|
|
1491
|
+
verify: boolean;
|
|
1492
|
+
}
|
|
1493
|
+
interface ReplicationPurgeReport {
|
|
1494
|
+
purged: string[];
|
|
1495
|
+
failed: Array<{
|
|
1496
|
+
prefix: string;
|
|
1497
|
+
code: string;
|
|
1498
|
+
}>;
|
|
1499
|
+
}
|
|
1500
|
+
interface PinnedKey {
|
|
1501
|
+
key: string;
|
|
1502
|
+
state: "in_flight" | "ambiguous";
|
|
1503
|
+
op: "put" | "delete";
|
|
1504
|
+
since: string;
|
|
1505
|
+
code?: string;
|
|
1506
|
+
likelyOrphaned: boolean;
|
|
1507
|
+
}
|
|
1508
|
+
interface ReplicaStatusEntry extends Partial<LocalReplicaStatus> {
|
|
1509
|
+
/** True when only opaque, ungated durable controls were inspected; no authority is known. */
|
|
1510
|
+
redacted?: boolean;
|
|
1511
|
+
prefix: string;
|
|
1512
|
+
state: ReplicaState;
|
|
1513
|
+
reason?: ReplicationReason;
|
|
1514
|
+
/** Set while state is `gated`. */
|
|
1515
|
+
gate?: ReplicaGateReason;
|
|
1516
|
+
errorCode?: string;
|
|
1517
|
+
pending: {
|
|
1518
|
+
inFlight: number;
|
|
1519
|
+
committed: number;
|
|
1520
|
+
ambiguous: number;
|
|
1521
|
+
};
|
|
1522
|
+
pinned: PinnedKey[];
|
|
1523
|
+
lagMs: number | null;
|
|
1524
|
+
}
|
|
1525
|
+
type ReplicaState = "idle" | "ready" | "grant_missing" | "runtime_unsupported" | "unavailable" | "revoked"
|
|
1526
|
+
/** The lifecycle gate refuses this session (REPLICA_GATED); reads use the network. */
|
|
1527
|
+
| "gated" | "closed";
|
|
1528
|
+
type ReplicationReason = "hit" | "absent" | "deleted" | "content_missing" | "coverage_incomplete" | "not_covered" | "pending_write" | "grant_missing" | "grant_expired" | "grant_revoked" | "grant_not_yet_valid" | "stale" | "replica_error" | "runtime_unsupported" | "replica_unavailable" | "unsupported_option" | "network_cursor" | "cursor_restart" | "aborted" | "REPLICA_BEHIND_OWN_WRITES" | "REPLICA_UNPROVEN_SINCE_START" | "NETWORK_REQUESTED"
|
|
1529
|
+
/** The lifecycle gate refused (REPLICA_GATED); the read went to the network. */
|
|
1530
|
+
| "replica_gated";
|
|
1531
|
+
type ReplicationEvent = {
|
|
1532
|
+
type: "replication.read";
|
|
1533
|
+
at: string;
|
|
1534
|
+
op: "get" | "list";
|
|
1535
|
+
space: string;
|
|
1536
|
+
key: string;
|
|
1537
|
+
replica: string | null;
|
|
1538
|
+
source: "replica" | "network" | "none";
|
|
1539
|
+
reason: ReplicationReason;
|
|
1540
|
+
outcome: "found" | "not_found" | "error";
|
|
1541
|
+
latencyMs: number;
|
|
1542
|
+
stalenessMs: number | null;
|
|
1543
|
+
coverage: ReplicaCoverage | null;
|
|
1544
|
+
authority: ReplicaAuthorityState | null;
|
|
1545
|
+
code?: string;
|
|
1546
|
+
syncError?: string;
|
|
1547
|
+
syncedBeforeRead?: boolean;
|
|
1548
|
+
pendingState?: "in_flight" | "committed" | "ambiguous";
|
|
1549
|
+
verify?: "match" | "diverged" | "error" | "aborted";
|
|
1550
|
+
count?: number;
|
|
1551
|
+
} | {
|
|
1552
|
+
type: "replication.write";
|
|
1553
|
+
at: string;
|
|
1554
|
+
op: "put" | "delete" | "batchPut";
|
|
1555
|
+
space: string;
|
|
1556
|
+
keys: string[];
|
|
1557
|
+
outcome: "committed" | "failed" | "ambiguous";
|
|
1558
|
+
code?: string;
|
|
1559
|
+
latencyMs: number;
|
|
1560
|
+
} | {
|
|
1561
|
+
type: "replication.sync";
|
|
1562
|
+
at: string;
|
|
1563
|
+
space: string;
|
|
1564
|
+
replica: string;
|
|
1565
|
+
/** `peer`: another tab/process committed (via `KVReplicaSpec.onPeerCommit`); no local work ran. */
|
|
1566
|
+
trigger: "start" | "interval" | "stale_read" | "manual" | "peer";
|
|
1567
|
+
/** Present and `peer` only for peer-commit notifications. */
|
|
1568
|
+
origin?: "peer";
|
|
1569
|
+
outcome: "ok" | "busy" | "error" | "aborted";
|
|
1570
|
+
class?: "offline" | "authority" | "storage" | "node" | "lifecycle";
|
|
1571
|
+
code?: string;
|
|
1572
|
+
durationMs: number;
|
|
1573
|
+
lagMs: number | null;
|
|
1574
|
+
pendingCleared?: number;
|
|
1575
|
+
pages?: number;
|
|
1576
|
+
changes?: number;
|
|
1577
|
+
deleted?: number;
|
|
1578
|
+
fetched?: number;
|
|
1579
|
+
contentMissing?: number;
|
|
1580
|
+
coverage?: ReplicaCoverage;
|
|
1581
|
+
} | {
|
|
1582
|
+
type: "replication.state";
|
|
1583
|
+
at: string;
|
|
1584
|
+
space?: string;
|
|
1585
|
+
replica?: string;
|
|
1586
|
+
state: "opened" | "grant_installed" | "grant_missing" | "runtime_unsupported" | "unavailable" | "revoked" | "recreated" | "purged" | "purge_failed" | "pending_store_error" | "pinned" | "pending_cleared" | "closed";
|
|
1587
|
+
code?: string;
|
|
1588
|
+
strategy?: "session" | "minted" | "installed";
|
|
1589
|
+
count?: number;
|
|
1590
|
+
keys?: string[];
|
|
1591
|
+
} | {
|
|
1592
|
+
/**
|
|
1593
|
+
* Grant lifetime and renewal (CONTRACT.md §3). Carries no raw account fields (`space`,
|
|
1594
|
+
* prefix or principal). This does not claim confidentiality against a raw browser-profile
|
|
1595
|
+
* reader who also has the persisted signed-in session.
|
|
1596
|
+
*/
|
|
1597
|
+
type: "replication.grant";
|
|
1598
|
+
at: string;
|
|
1599
|
+
/** Opaque id from an opened `ReplicaGatedHandle`, never inferred from raw space/prefix. */
|
|
1600
|
+
replicaId: string;
|
|
1601
|
+
action: "minted" | "renewed"
|
|
1602
|
+
/** The node or authority refused renewal; current grant serves until effective expiry. */
|
|
1603
|
+
| "renewal_refused"
|
|
1604
|
+
/** Offline or the authority can't derive; retried on the next trigger. */
|
|
1605
|
+
| "renewal_deferred"
|
|
1606
|
+
/** An overlong session/restored grant was capped locally or replaced by a derived grant. */
|
|
1607
|
+
| "capped" | "expired" | "consent_deadline_reached";
|
|
1608
|
+
code?: string;
|
|
1609
|
+
/** ISO instants. */
|
|
1610
|
+
effectiveExpiresAt?: string;
|
|
1611
|
+
consentDeadline?: string;
|
|
1612
|
+
/** Duration passed to `mint` (`expiryMs`). */
|
|
1613
|
+
expiryMs?: number;
|
|
1614
|
+
} | {
|
|
1615
|
+
/** Sign-out / switch / consent-expiry lifecycle (CONTRACT.md §2). No raw account fields. */
|
|
1616
|
+
type: "replication.lifecycle";
|
|
1617
|
+
at: string;
|
|
1618
|
+
/** Replica id when the step concerns one partition (same scheme as `replication.grant`). */
|
|
1619
|
+
replicaId?: string;
|
|
1620
|
+
action: "denied"
|
|
1621
|
+
/** A stale caller's deny: disabled in memory only; gate untouched. */
|
|
1622
|
+
| "deny_stale" | "deny_failed" | "database_deleted" | "marker_set" | "marker_cleared" | "unguarded" | "drained" | "erased" | "erase_failed" | "activation_blocked" | "activated" | "recovered";
|
|
1623
|
+
reason?: "sign_out" | "account_switch" | "consent_expired" | "startup";
|
|
1624
|
+
gate?: ReplicaGateReason;
|
|
1625
|
+
/** Partitions/intents affected by this step. */
|
|
1626
|
+
partitions?: number;
|
|
1627
|
+
/** Intents still outstanding after this step. */
|
|
1628
|
+
remaining?: number;
|
|
1629
|
+
code?: string;
|
|
1630
|
+
} | {
|
|
1631
|
+
type: "replication.divergence";
|
|
1632
|
+
at: string;
|
|
1633
|
+
op: "get" | "list";
|
|
1634
|
+
space: string;
|
|
1635
|
+
key: string;
|
|
1636
|
+
replica: string;
|
|
1637
|
+
kind: "value" | "missing_local" | "extra_local" | "keys" | "order";
|
|
1638
|
+
localEtag?: string | null;
|
|
1639
|
+
networkEtag?: string | null;
|
|
1640
|
+
localOnly?: string[];
|
|
1641
|
+
networkOnly?: string[];
|
|
1642
|
+
stalenessMs: number | null;
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
declare const CLEAR_PENDING_WARNING = "Clearing pending writes surrenders read-your-writes protection for the cleared keys. If a write whose record was cleared had an unknown outcome or never finished, it may commit now or later; this device can serve an older value until the next sync after that commit, as it would for a write from another device.";
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* Pure canonicalization: lower-cases only EIP-155 address segments and preserves all other
|
|
1649
|
+
* space/principal bytes. Rejects non-HTTP(S) hosts, credentials, query/fragment, and non-full
|
|
1650
|
+
* TinyCloud space ids or non-pkh principals.
|
|
1651
|
+
*/
|
|
1652
|
+
declare function canonicalReplicationIdentity(raw: ReplicationIdentity): ReplicationIdentity;
|
|
1653
|
+
/** Stable in-memory key; durable adapters hash its UTF-8 bytes with their platform crypto. */
|
|
1654
|
+
declare function replicationIdentityKey(id: ReplicationIdentity): string;
|
|
1655
|
+
|
|
1656
|
+
/** Whether replicating this space/prefix can copy encrypted secret material to disk. */
|
|
1657
|
+
declare function requiresSecretsOptIn(space: string, prefix: string): boolean;
|
|
1658
|
+
|
|
1659
|
+
export { type ReplicaQuotaLedger as $, type AuthorityRefusal as A, type ReplicaDevice as B, CLEAR_PENDING_WARNING as C, type ReplicaEraseReport as D, type ReplicaGateAccess as E, type ReplicaGateReason as F, type ReplicaGateScope as G, type ReplicaGateSnapshot as H, type ReplicaGateTx as I, type ReplicaGatedError as J, type KVReplicationDeps as K, type LocalGetResult as L, type ReplicaGatedHandle as M, type ReplicaGatedSpec as N, type ReplicaGatedStorage as O, type PendingWriteStore as P, type ReplicaGrantInfo as Q, type ReplicationIdentity as R, type ReplicaGuardedStore as S, type ReplicaInspectedStatus as T, type ReplicaLifecycleGate as U, type ReplicaPageAdmission as V, type ReplicaPartitionEraser as W, type ReplicaPartitionIndexEntry as X, type ReplicaPeerCommit as Y, type ReplicaPrivateGateHash as Z, type ReplicaPurgeIntent as _, type KVReplicationController as a, type ReplicaQuotaSpec as a0, type ReplicaSessionStamp as a1, type ReplicaStampedReply as a2, type ReplicaState as a3, type ReplicaStatusEntry as a4, type ReplicationAuthority as a5, type ReplicationControl as a6, type ReplicationEvent as a7, type ReplicationOptions as a8, type ReplicationPurgeReport as a9, type KVResponseHeaders as aA, type KVServiceConfig as aB, type KVSignedReadUrlResponse as aC, type ReplicationReason as aa, type ReplicationScheduler as ab, type ResolvedReplicationOptions as ac, type SaltedHash as ad, canonicalReplicationIdentity as ae, replicationIdentityKey as af, requiresSecretsOptIn as ag, DEFAULT_SIGNED_READ_URL_EXPIRY_MS as ah, KVAction as ai, type KVActionType as aj, type KVBatchPutItem as ak, type KVBatchPutOptions as al, type KVBatchPutResponse as am, type KVBatchReadResponse as an, type KVChange as ao, type KVChangesAuthority as ap, type KVChangesOptions as aq, type KVChangesResponse as ar, type KVCreateSignedReadUrlOptions as as, type KVDeleteOptions as at, type KVGetOptions as au, type KVHeadOptions as av, type KVListOptions as aw, type KVListResponse as ax, type KVPutOptions as ay, type KVResponse as az, type KVListPage as b, type KVReadThrough as c, type KVReplicaHandle as d, type KVReplicaSpec as e, type KVReplicaStorage as f, type LocalListResult as g, type LocalReadMeta as h, type LocalReplicaStatus as i, type LocalSyncResult as j, type PendingWriteRecord as k, type PendingWriteState as l, type PinnedKey as m, type PurgeTarget as n, type ReplicaAccountGate as o, type ReplicaAdmitHook as p, type ReplicaAuthorityState as q, type ReplicaBrowserGateAccess as r, type ReplicaBrowserGateSnapshot as s, type ReplicaBrowserGateTx as t, type ReplicaBrowserPageAdmission as u, type ReplicaClock as v, type ReplicaCoverage as w, type ReplicaDeliveryDecision as x, type ReplicaDenyInput as y, type ReplicaDenyOutcome as z };
|