@drakkar.software/starfish-client 3.0.0-alpha.7 → 3.0.0-alpha.9
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/README.md +21 -0
- package/dist/append-log.d.ts +158 -0
- package/dist/bindings/legend.d.ts +23 -0
- package/dist/bindings/legend.js +32 -0
- package/dist/bindings/legend.js.map +2 -2
- package/dist/bindings/zustand.d.ts +38 -0
- package/dist/bindings/zustand.js +235 -50
- package/dist/bindings/zustand.js.map +3 -3
- package/dist/client.d.ts +60 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +313 -52
- package/dist/index.js.map +4 -4
- package/dist/mobile-lifecycle.d.ts +28 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { configurePlatform } from "@drakkar.software/starfish-protocol";
|
|
3
|
-
import { stableStringify as
|
|
3
|
+
import { stableStringify as stableStringify2, computeHash } from "@drakkar.software/starfish-protocol";
|
|
4
4
|
import { buildRevocationList, revocationListCanonicalSigningInput } from "@drakkar.software/starfish-protocol";
|
|
5
5
|
|
|
6
6
|
// src/client.ts
|
|
7
7
|
import {
|
|
8
|
+
AUTHOR_PUBKEY_FIELD,
|
|
9
|
+
AUTHOR_SIGNATURE_FIELD,
|
|
10
|
+
DATA_FIELD,
|
|
11
|
+
TS_FIELD,
|
|
12
|
+
BASE_HASH_FIELD,
|
|
13
|
+
PUSH_PATH_PREFIX,
|
|
14
|
+
HEADER_AUTHORIZATION,
|
|
15
|
+
HEADER_SIG,
|
|
16
|
+
HEADER_TS,
|
|
17
|
+
HEADER_NONCE,
|
|
18
|
+
HEADER_ALG,
|
|
19
|
+
HEADER_PUB,
|
|
20
|
+
HEADER_CONTENT_TYPE,
|
|
21
|
+
HEADER_ACCEPT,
|
|
8
22
|
DEFAULT_ALG,
|
|
23
|
+
signAppendAuthor,
|
|
9
24
|
signRequest,
|
|
10
25
|
stableStringify
|
|
11
26
|
} from "@drakkar.software/starfish-protocol";
|
|
@@ -28,6 +43,9 @@ var StarfishHttpError = class extends Error {
|
|
|
28
43
|
|
|
29
44
|
// src/client.ts
|
|
30
45
|
var APPEND_DEFAULT_FIELD = "items";
|
|
46
|
+
function stripPushPrefix(path) {
|
|
47
|
+
return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path;
|
|
48
|
+
}
|
|
31
49
|
function encodeCapAuth(cap) {
|
|
32
50
|
const json = stableStringify(cap);
|
|
33
51
|
if (typeof btoa === "function") {
|
|
@@ -98,29 +116,54 @@ var StarfishClient = class {
|
|
|
98
116
|
* The host bound into the signature is derived from `baseUrl` once per call.
|
|
99
117
|
*/
|
|
100
118
|
async buildAuthHeaders(method, pathAndQuery, body) {
|
|
101
|
-
if (this.capProvider) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
119
|
+
if (!this.capProvider) return {};
|
|
120
|
+
const capCtx = await this.capProvider.getCap();
|
|
121
|
+
return this.capRequestHeaders(capCtx, method, pathAndQuery, body);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build the request-signing headers from an ALREADY-fetched cap context. Split
|
|
125
|
+
* out of {@link buildAuthHeaders} so {@link append} can fetch the cap once and
|
|
126
|
+
* reuse it for BOTH the author signature (over the element data) and the
|
|
127
|
+
* request signature (over the body), without redeeming the cap twice — a
|
|
128
|
+
* second `getCap()` could rotate keys and break the `authorPubkey ===
|
|
129
|
+
* presenter` bind the server checks.
|
|
130
|
+
*/
|
|
131
|
+
async capRequestHeaders(capCtx, method, pathAndQuery, body) {
|
|
132
|
+
const { cap, devEdPrivHex, pubHex, presenterAlg } = capCtx;
|
|
133
|
+
const req = {
|
|
134
|
+
method,
|
|
135
|
+
pathAndQuery,
|
|
136
|
+
body,
|
|
137
|
+
host: this.signingHost()
|
|
138
|
+
};
|
|
139
|
+
const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
|
|
140
|
+
const { alg, sig, ts, nonce } = await signRequest(req, devEdPrivHex, {
|
|
141
|
+
alg: signAlg
|
|
142
|
+
});
|
|
143
|
+
const headers = {
|
|
144
|
+
[HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,
|
|
145
|
+
[HEADER_SIG]: sig,
|
|
146
|
+
[HEADER_TS]: String(ts),
|
|
147
|
+
[HEADER_NONCE]: nonce,
|
|
148
|
+
[HEADER_ALG]: alg
|
|
149
|
+
};
|
|
150
|
+
if (pubHex !== void 0) headers[HEADER_PUB] = pubHex;
|
|
151
|
+
return headers;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the author public key to attach to a signed append: the redeemer's
|
|
155
|
+
* `pubHex` for an audience cap, else the cert subject `cap.sub` for a
|
|
156
|
+
* device/member cap. This is the SAME key that signs the request, so a server
|
|
157
|
+
* enforcing author proof can bind the stored element to its writer. Returns
|
|
158
|
+
* undefined only for a (malformed) cap with neither — the append then goes
|
|
159
|
+
* unsigned and a server requiring signatures rejects it.
|
|
160
|
+
*/
|
|
161
|
+
appendAuthorKey(capCtx) {
|
|
162
|
+
const { cap, pubHex, presenterAlg } = capCtx;
|
|
163
|
+
const authorPubHex = pubHex ?? cap.sub;
|
|
164
|
+
if (authorPubHex === void 0) return null;
|
|
165
|
+
const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
|
|
166
|
+
return { authorPubHex, signAlg };
|
|
124
167
|
}
|
|
125
168
|
async pull(path, checkpointOrOptions) {
|
|
126
169
|
let pathAndQuery = this.applyNamespace(path);
|
|
@@ -155,7 +198,7 @@ var StarfishClient = class {
|
|
|
155
198
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
156
199
|
const res = await this.fetch(url, {
|
|
157
200
|
method: "GET",
|
|
158
|
-
headers: {
|
|
201
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
159
202
|
});
|
|
160
203
|
if (!res.ok) {
|
|
161
204
|
throw new StarfishHttpError(res.status, await res.text());
|
|
@@ -167,28 +210,62 @@ var StarfishClient = class {
|
|
|
167
210
|
}
|
|
168
211
|
return result;
|
|
169
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Pull several collections in one round-trip via `/batch/pull`. `collections`
|
|
215
|
+
* is the list of collection names; `opts.params` supplies path params per
|
|
216
|
+
* collection (serialized to a URL-encoded JSON `params` query). The server
|
|
217
|
+
* auto-fills the `{identity}` param from the authenticated caller, so per-user
|
|
218
|
+
* collections need no params. Returns a map of collection name → its pulled
|
|
219
|
+
* document or a per-collection `{ error }`. Honors the configured namespace.
|
|
220
|
+
*
|
|
221
|
+
* Note: not append/checkpoint-aware — for incremental append-only reads use
|
|
222
|
+
* `pull(path, { since })` (or `AppendLogCursor`) per collection.
|
|
223
|
+
*/
|
|
224
|
+
async batchPull(collections, opts = {}) {
|
|
225
|
+
const search = new URLSearchParams();
|
|
226
|
+
search.set("collections", collections.join(","));
|
|
227
|
+
if (opts.params && Object.keys(opts.params).length > 0) {
|
|
228
|
+
search.set("params", JSON.stringify(opts.params));
|
|
229
|
+
}
|
|
230
|
+
const pathAndQuery = `${this.applyNamespace("/batch/pull")}?${search.toString()}`;
|
|
231
|
+
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
232
|
+
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
233
|
+
const res = await this.fetch(url, {
|
|
234
|
+
method: "GET",
|
|
235
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
236
|
+
});
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
throw new StarfishHttpError(res.status, await res.text());
|
|
239
|
+
}
|
|
240
|
+
return await res.json();
|
|
241
|
+
}
|
|
170
242
|
/**
|
|
171
243
|
* Push synced data to the server.
|
|
172
244
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
173
245
|
* @param data - The full document data to push
|
|
174
246
|
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
175
247
|
*
|
|
176
|
-
* v3 author
|
|
177
|
-
*
|
|
248
|
+
* v3 author proof (`authorPubkey` + `authorSignature`) is passed via `author`
|
|
249
|
+
* (produced by `SyncManager` when a `signer` is configured) and sent as
|
|
250
|
+
* top-level body siblings of `data`, where the server verifies it.
|
|
178
251
|
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
179
252
|
*/
|
|
180
|
-
async push(path, data, baseHash) {
|
|
253
|
+
async push(path, data, baseHash, author) {
|
|
181
254
|
const body = JSON.stringify({
|
|
182
|
-
data,
|
|
183
|
-
baseHash
|
|
255
|
+
[DATA_FIELD]: data,
|
|
256
|
+
[BASE_HASH_FIELD]: baseHash,
|
|
257
|
+
...author && {
|
|
258
|
+
[AUTHOR_PUBKEY_FIELD]: author.authorPubkey,
|
|
259
|
+
[AUTHOR_SIGNATURE_FIELD]: author.authorSignature
|
|
260
|
+
}
|
|
184
261
|
});
|
|
185
262
|
const sendPath = this.applyNamespace(path);
|
|
186
263
|
const authHeaders = await this.buildAuthHeaders("POST", sendPath, body);
|
|
187
264
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
188
265
|
method: "POST",
|
|
189
266
|
headers: {
|
|
190
|
-
|
|
191
|
-
|
|
267
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
268
|
+
[HEADER_ACCEPT]: "application/json",
|
|
192
269
|
...authHeaders
|
|
193
270
|
},
|
|
194
271
|
body
|
|
@@ -221,16 +298,32 @@ var StarfishClient = class {
|
|
|
221
298
|
* cap is reached (partition by a path parameter for higher volume).
|
|
222
299
|
*/
|
|
223
300
|
async append(path, data, opts = {}) {
|
|
224
|
-
const bodyObj = { data };
|
|
225
|
-
if (opts.ts !== void 0) bodyObj["ts"] = opts.ts;
|
|
226
|
-
const body = JSON.stringify(bodyObj);
|
|
227
301
|
const sendPath = this.applyNamespace(path);
|
|
228
|
-
const
|
|
302
|
+
const bodyObj = { [DATA_FIELD]: data };
|
|
303
|
+
if (opts.ts !== void 0) bodyObj[TS_FIELD] = opts.ts;
|
|
304
|
+
const capCtx = this.capProvider ? await this.capProvider.getCap() : null;
|
|
305
|
+
if (capCtx) {
|
|
306
|
+
const authorKey = this.appendAuthorKey(capCtx);
|
|
307
|
+
if (authorKey) {
|
|
308
|
+
const documentKey = stripPushPrefix(path);
|
|
309
|
+
const { authorPubkey, authorSignature } = signAppendAuthor(
|
|
310
|
+
documentKey,
|
|
311
|
+
data,
|
|
312
|
+
authorKey.authorPubHex,
|
|
313
|
+
capCtx.devEdPrivHex,
|
|
314
|
+
authorKey.signAlg
|
|
315
|
+
);
|
|
316
|
+
bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey;
|
|
317
|
+
bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const body = JSON.stringify(bodyObj);
|
|
321
|
+
const authHeaders = capCtx ? await this.capRequestHeaders(capCtx, "POST", sendPath, body) : {};
|
|
229
322
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
230
323
|
method: "POST",
|
|
231
324
|
headers: {
|
|
232
|
-
|
|
233
|
-
|
|
325
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
326
|
+
[HEADER_ACCEPT]: "application/json",
|
|
234
327
|
...authHeaders
|
|
235
328
|
},
|
|
236
329
|
body
|
|
@@ -249,13 +342,13 @@ var StarfishClient = class {
|
|
|
249
342
|
const authHeaders = await this.buildAuthHeaders("GET", sendPath, void 0);
|
|
250
343
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
251
344
|
method: "GET",
|
|
252
|
-
headers: {
|
|
345
|
+
headers: { [HEADER_ACCEPT]: "*/*", ...authHeaders }
|
|
253
346
|
});
|
|
254
347
|
if (!res.ok) {
|
|
255
348
|
throw new StarfishHttpError(res.status, await res.text());
|
|
256
349
|
}
|
|
257
350
|
const etag = res.headers.get("ETag")?.replace(/"/g, "") ?? null;
|
|
258
|
-
const contentType = res.headers.get(
|
|
351
|
+
const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? "application/octet-stream";
|
|
259
352
|
const data = await res.arrayBuffer();
|
|
260
353
|
return { data, hash: etag, contentType };
|
|
261
354
|
}
|
|
@@ -269,8 +362,8 @@ var StarfishClient = class {
|
|
|
269
362
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
270
363
|
method: "POST",
|
|
271
364
|
headers: {
|
|
272
|
-
|
|
273
|
-
|
|
365
|
+
[HEADER_CONTENT_TYPE]: contentType,
|
|
366
|
+
[HEADER_ACCEPT]: "application/json",
|
|
274
367
|
...authHeaders
|
|
275
368
|
},
|
|
276
369
|
body: data
|
|
@@ -283,7 +376,13 @@ var StarfishClient = class {
|
|
|
283
376
|
};
|
|
284
377
|
|
|
285
378
|
// src/sync.ts
|
|
286
|
-
import {
|
|
379
|
+
import {
|
|
380
|
+
AUTHOR_PUBKEY_FIELD as AUTHOR_PUBKEY_FIELD2,
|
|
381
|
+
AUTHOR_SIGNATURE_FIELD as AUTHOR_SIGNATURE_FIELD2,
|
|
382
|
+
deepMerge,
|
|
383
|
+
docAuthorCanonicalInput,
|
|
384
|
+
getBase64
|
|
385
|
+
} from "@drakkar.software/starfish-protocol";
|
|
287
386
|
|
|
288
387
|
// src/validate.ts
|
|
289
388
|
var ValidationError = class extends Error {
|
|
@@ -395,23 +494,24 @@ var SyncManager = class {
|
|
|
395
494
|
try {
|
|
396
495
|
const sealed = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
397
496
|
if (this.aborted) throw new AbortError();
|
|
398
|
-
let
|
|
497
|
+
let author;
|
|
399
498
|
if (this.signer) {
|
|
400
499
|
const { devEdPubHex, sign } = await this.signer.getSigner();
|
|
401
500
|
if (this.aborted) throw new AbortError();
|
|
402
|
-
const
|
|
501
|
+
const documentKey = stripPushPrefix(this.pushPath);
|
|
502
|
+
const canonical = docAuthorCanonicalInput(documentKey, sealed);
|
|
403
503
|
const sigBytes = await sign(new TextEncoder().encode(canonical));
|
|
404
504
|
if (this.aborted) throw new AbortError();
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
authorSignature: getBase64().encode(sigBytes)
|
|
505
|
+
author = {
|
|
506
|
+
[AUTHOR_PUBKEY_FIELD2]: devEdPubHex,
|
|
507
|
+
[AUTHOR_SIGNATURE_FIELD2]: getBase64().encode(sigBytes)
|
|
409
508
|
};
|
|
410
509
|
}
|
|
411
510
|
const result = await this.client.push(
|
|
412
511
|
this.pushPath,
|
|
413
|
-
|
|
414
|
-
this.lastHash
|
|
512
|
+
sealed,
|
|
513
|
+
this.lastHash,
|
|
514
|
+
author
|
|
415
515
|
);
|
|
416
516
|
if (this.aborted) throw new AbortError();
|
|
417
517
|
this.lastHash = result.hash;
|
|
@@ -453,6 +553,140 @@ var SyncManager = class {
|
|
|
453
553
|
}
|
|
454
554
|
};
|
|
455
555
|
|
|
556
|
+
// src/append-log.ts
|
|
557
|
+
import {
|
|
558
|
+
DEFAULT_ALG as DEFAULT_ALG2,
|
|
559
|
+
verifyAppendAuthor
|
|
560
|
+
} from "@drakkar.software/starfish-protocol";
|
|
561
|
+
var PULL_PATH_PREFIX = "/pull/";
|
|
562
|
+
function stripPullPrefix(path) {
|
|
563
|
+
return path.startsWith(PULL_PATH_PREFIX) ? path.slice(PULL_PATH_PREFIX.length) : path;
|
|
564
|
+
}
|
|
565
|
+
var AppendAuthorError = class extends Error {
|
|
566
|
+
constructor(ts) {
|
|
567
|
+
super(`append element author verification failed (ts=${ts})`);
|
|
568
|
+
this.ts = ts;
|
|
569
|
+
this.name = "AppendAuthorError";
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
function checkpointOf(items) {
|
|
573
|
+
let max = 0;
|
|
574
|
+
for (const it of items) if (it.ts > max) max = it.ts;
|
|
575
|
+
return max;
|
|
576
|
+
}
|
|
577
|
+
var AppendLogCursor = class {
|
|
578
|
+
client;
|
|
579
|
+
pullPath;
|
|
580
|
+
appendField;
|
|
581
|
+
encryptor;
|
|
582
|
+
verifyAuthor;
|
|
583
|
+
documentKey;
|
|
584
|
+
logger;
|
|
585
|
+
loggerName;
|
|
586
|
+
items;
|
|
587
|
+
lastCheckpoint;
|
|
588
|
+
constructor(options) {
|
|
589
|
+
this.client = options.client;
|
|
590
|
+
this.pullPath = options.pullPath;
|
|
591
|
+
this.appendField = options.appendField ?? "items";
|
|
592
|
+
this.encryptor = options.encryptor;
|
|
593
|
+
this.verifyAuthor = options.verifyAuthor;
|
|
594
|
+
this.documentKey = stripPullPrefix(options.pullPath);
|
|
595
|
+
this.logger = options.logger;
|
|
596
|
+
this.loggerName = options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
|
|
597
|
+
const seed = options.initialItems ?? [];
|
|
598
|
+
const seedCheckpoint = checkpointOf(seed);
|
|
599
|
+
if (options.since != null) {
|
|
600
|
+
if (options.since < 0) throw new Error("since must be non-negative");
|
|
601
|
+
if (options.since < seedCheckpoint) {
|
|
602
|
+
throw new Error("since must be >= the max ts of initialItems");
|
|
603
|
+
}
|
|
604
|
+
this.lastCheckpoint = options.since;
|
|
605
|
+
} else {
|
|
606
|
+
this.lastCheckpoint = seedCheckpoint;
|
|
607
|
+
}
|
|
608
|
+
this.items = [...seed];
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Fetch elements newer than the current checkpoint, verify + decrypt them,
|
|
612
|
+
* append them to the local log, and return ONLY the newly-fetched batch.
|
|
613
|
+
*
|
|
614
|
+
* Atomic: the batch is fully verified and decrypted into a local before any
|
|
615
|
+
* state mutation, so a verify/decrypt failure throws without advancing the
|
|
616
|
+
* checkpoint past elements that could never be re-fetched.
|
|
617
|
+
*
|
|
618
|
+
* Not safe to call concurrently: like `SyncManager.pull`, overlapping calls
|
|
619
|
+
* read the same checkpoint and would fetch — and append — the same window twice.
|
|
620
|
+
*/
|
|
621
|
+
async pull() {
|
|
622
|
+
this.logger?.pullStart(this.loggerName);
|
|
623
|
+
const start = performance.now();
|
|
624
|
+
try {
|
|
625
|
+
const since = this.lastCheckpoint;
|
|
626
|
+
const opts = since > 0 ? { appendField: this.appendField, since } : { appendField: this.appendField };
|
|
627
|
+
const raw = await this.client.pull(this.pullPath, opts);
|
|
628
|
+
const batch = [];
|
|
629
|
+
let maxTs = since;
|
|
630
|
+
for (const el of raw) {
|
|
631
|
+
if (since > 0 && el.ts <= since) continue;
|
|
632
|
+
this.verifyOne(el);
|
|
633
|
+
const data = this.encryptor ? await this.encryptor.decrypt(el.data) : el.data;
|
|
634
|
+
const out = { ts: el.ts, data };
|
|
635
|
+
if (el.authorPubkey !== void 0) out.authorPubkey = el.authorPubkey;
|
|
636
|
+
if (el.authorSignature !== void 0) out.authorSignature = el.authorSignature;
|
|
637
|
+
batch.push(out);
|
|
638
|
+
if (el.ts > maxTs) maxTs = el.ts;
|
|
639
|
+
}
|
|
640
|
+
this.items.push(...batch);
|
|
641
|
+
this.lastCheckpoint = maxTs;
|
|
642
|
+
this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start));
|
|
643
|
+
return batch;
|
|
644
|
+
} catch (err) {
|
|
645
|
+
this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err));
|
|
646
|
+
throw err;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/** Verify a single element's author signature over its RAW (pre-decryption)
|
|
650
|
+
* `data`. Throws {@link AppendAuthorError} on any failure. No-op when
|
|
651
|
+
* verification is disabled. */
|
|
652
|
+
verifyOne(el) {
|
|
653
|
+
if (!this.verifyAuthor) return;
|
|
654
|
+
const policy = typeof this.verifyAuthor === "object" ? this.verifyAuthor : {};
|
|
655
|
+
const { authorPubkey, authorSignature } = el;
|
|
656
|
+
if (!authorPubkey || !authorSignature) throw new AppendAuthorError(el.ts);
|
|
657
|
+
if (policy.expectedAuthorPubkey && authorPubkey.toLowerCase() !== policy.expectedAuthorPubkey.toLowerCase()) {
|
|
658
|
+
throw new AppendAuthorError(el.ts);
|
|
659
|
+
}
|
|
660
|
+
const ok = verifyAppendAuthor(
|
|
661
|
+
this.documentKey,
|
|
662
|
+
el.data,
|
|
663
|
+
authorPubkey,
|
|
664
|
+
authorSignature,
|
|
665
|
+
policy.alg ?? DEFAULT_ALG2
|
|
666
|
+
);
|
|
667
|
+
if (!ok) throw new AppendAuthorError(el.ts);
|
|
668
|
+
}
|
|
669
|
+
/** The full accumulated log (a shallow copy), in `ts` order. */
|
|
670
|
+
getItems() {
|
|
671
|
+
return [...this.items];
|
|
672
|
+
}
|
|
673
|
+
/** The current checkpoint: the max `ts` held (the next pull's `since`). `0`
|
|
674
|
+
* when nothing has been pulled or seeded. */
|
|
675
|
+
getCheckpoint() {
|
|
676
|
+
return this.lastCheckpoint;
|
|
677
|
+
}
|
|
678
|
+
/** Restore the checkpoint without seeding items — for persistence layers that
|
|
679
|
+
* store only the checkpoint. Used to resume incrementally across restarts.
|
|
680
|
+
* Rejects a value below the max `ts` already held: rewinding would make the
|
|
681
|
+
* next pull re-deliver, and duplicate, elements the cursor already has. */
|
|
682
|
+
setCheckpoint(ts) {
|
|
683
|
+
if (ts < checkpointOf(this.items)) {
|
|
684
|
+
throw new Error("checkpoint must be >= the max ts already held");
|
|
685
|
+
}
|
|
686
|
+
this.lastCheckpoint = ts;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
456
690
|
// src/index.ts
|
|
457
691
|
import { ENCRYPTED_KEY } from "@drakkar.software/starfish-protocol";
|
|
458
692
|
|
|
@@ -1238,6 +1472,29 @@ function createMobileLifecycle(store, deps, options = {}) {
|
|
|
1238
1472
|
netUnsub?.();
|
|
1239
1473
|
};
|
|
1240
1474
|
}
|
|
1475
|
+
function createAppendLogMobileLifecycle(store, deps, options = {}) {
|
|
1476
|
+
const { pullOnForeground = true } = options;
|
|
1477
|
+
const appSub = deps.appState.addEventListener("change", (appState) => {
|
|
1478
|
+
if (appState === "active" && pullOnForeground) {
|
|
1479
|
+
const { online, loading } = store.getState();
|
|
1480
|
+
if (online && !loading) {
|
|
1481
|
+
store.getState().pull().catch((err) => {
|
|
1482
|
+
console.error("[Starfish] foreground log pull failed:", err);
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
let netUnsub = null;
|
|
1488
|
+
if (deps.netInfo) {
|
|
1489
|
+
netUnsub = deps.netInfo.addEventListener(({ isConnected }) => {
|
|
1490
|
+
store.getState().setOnline(!!isConnected);
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
return () => {
|
|
1494
|
+
appSub.remove();
|
|
1495
|
+
netUnsub?.();
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1241
1498
|
|
|
1242
1499
|
// src/multi-store.ts
|
|
1243
1500
|
function createMultiStoreSync(options) {
|
|
@@ -1290,6 +1547,8 @@ function createMultiStoreSync(options) {
|
|
|
1290
1547
|
}
|
|
1291
1548
|
export {
|
|
1292
1549
|
AbortError,
|
|
1550
|
+
AppendAuthorError,
|
|
1551
|
+
AppendLogCursor,
|
|
1293
1552
|
ConflictError,
|
|
1294
1553
|
ENCRYPTED_KEY,
|
|
1295
1554
|
SnapshotHistory,
|
|
@@ -1298,10 +1557,12 @@ export {
|
|
|
1298
1557
|
SyncManager,
|
|
1299
1558
|
ValidationError,
|
|
1300
1559
|
buildRevocationList,
|
|
1560
|
+
checkpointOf,
|
|
1301
1561
|
classifyError,
|
|
1302
1562
|
computeHash,
|
|
1303
1563
|
configurePlatform,
|
|
1304
1564
|
consoleSyncLogger,
|
|
1565
|
+
createAppendLogMobileLifecycle,
|
|
1305
1566
|
createDebouncedPush,
|
|
1306
1567
|
createDebouncedSync,
|
|
1307
1568
|
createDedupFetch,
|
|
@@ -1325,7 +1586,7 @@ export {
|
|
|
1325
1586
|
registerBackgroundSync,
|
|
1326
1587
|
registerServiceWorker,
|
|
1327
1588
|
revocationListCanonicalSigningInput,
|
|
1328
|
-
|
|
1589
|
+
stableStringify2 as stableStringify,
|
|
1329
1590
|
startAdaptivePolling,
|
|
1330
1591
|
startPolling,
|
|
1331
1592
|
timestampWinner,
|