@drakkar.software/starfish-client 3.0.0-alpha.8 → 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 +114 -0
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +33 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +190 -0
- 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
|
@@ -210,6 +210,35 @@ var StarfishClient = class {
|
|
|
210
210
|
}
|
|
211
211
|
return result;
|
|
212
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
|
+
}
|
|
213
242
|
/**
|
|
214
243
|
* Push synced data to the server.
|
|
215
244
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
@@ -524,6 +553,140 @@ var SyncManager = class {
|
|
|
524
553
|
}
|
|
525
554
|
};
|
|
526
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
|
+
|
|
527
690
|
// src/index.ts
|
|
528
691
|
import { ENCRYPTED_KEY } from "@drakkar.software/starfish-protocol";
|
|
529
692
|
|
|
@@ -1309,6 +1472,29 @@ function createMobileLifecycle(store, deps, options = {}) {
|
|
|
1309
1472
|
netUnsub?.();
|
|
1310
1473
|
};
|
|
1311
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
|
+
}
|
|
1312
1498
|
|
|
1313
1499
|
// src/multi-store.ts
|
|
1314
1500
|
function createMultiStoreSync(options) {
|
|
@@ -1361,6 +1547,8 @@ function createMultiStoreSync(options) {
|
|
|
1361
1547
|
}
|
|
1362
1548
|
export {
|
|
1363
1549
|
AbortError,
|
|
1550
|
+
AppendAuthorError,
|
|
1551
|
+
AppendLogCursor,
|
|
1364
1552
|
ConflictError,
|
|
1365
1553
|
ENCRYPTED_KEY,
|
|
1366
1554
|
SnapshotHistory,
|
|
@@ -1369,10 +1557,12 @@ export {
|
|
|
1369
1557
|
SyncManager,
|
|
1370
1558
|
ValidationError,
|
|
1371
1559
|
buildRevocationList,
|
|
1560
|
+
checkpointOf,
|
|
1372
1561
|
classifyError,
|
|
1373
1562
|
computeHash,
|
|
1374
1563
|
configurePlatform,
|
|
1375
1564
|
consoleSyncLogger,
|
|
1565
|
+
createAppendLogMobileLifecycle,
|
|
1376
1566
|
createDebouncedPush,
|
|
1377
1567
|
createDebouncedSync,
|
|
1378
1568
|
createDedupFetch,
|