@drakkar.software/starfish-client 3.0.0-alpha.0 → 3.0.0-alpha.2
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/append.d.ts +50 -0
- package/dist/background-sync.js +29 -0
- package/dist/bindings/broadcast.d.ts +19 -0
- package/dist/bindings/broadcast.js +65 -0
- package/dist/bindings/react.d.ts +12 -0
- package/dist/bindings/react.js +25 -0
- package/dist/bindings/suspense.js +49 -0
- package/dist/bindings/zustand.js +40 -2
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +20 -0
- package/dist/client.js +112 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +18 -0
- package/dist/crypto.js +49 -0
- package/dist/debounced-sync.js +120 -0
- package/dist/dedup.js +35 -0
- package/dist/entitlements.js +41 -0
- package/dist/export.js +115 -0
- package/dist/group-crypto.d.ts +111 -0
- package/dist/group-crypto.js +205 -0
- package/dist/group-crypto.js.map +7 -0
- package/dist/history.js +61 -0
- package/dist/identity.d.ts +82 -4
- package/dist/identity.js +354 -2
- package/dist/identity.js.map +4 -4
- package/dist/index.js +40 -2
- package/dist/index.js.map +2 -2
- package/dist/logger.js +80 -0
- package/dist/migrate.js +38 -0
- package/dist/mobile-lifecycle.js +55 -0
- package/dist/multi-store.js +92 -0
- package/dist/polling.js +52 -0
- package/dist/resolvers.js +223 -0
- package/dist/service-worker.js +55 -0
- package/dist/storage/indexeddb.js +59 -0
- package/dist/sync.js +127 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +18 -0
- package/dist/validate.js +28 -0
- package/package.json +2 -2
- package/dist/_crypto_helpers.d.ts +0 -4
- package/dist/cap-mint.d.ts +0 -20
- package/dist/cap-mint.js +0 -12
- package/dist/cap-mint.js.map +0 -7
- package/dist/directory.d.ts +0 -9
- package/dist/directory.js +0 -24
- package/dist/directory.js.map +0 -7
- package/dist/keyring.d.ts +0 -6
- package/dist/keyring.js +0 -26
- package/dist/keyring.js.map +0 -7
- package/dist/pairing.d.ts +0 -6
- package/dist/pairing.js +0 -26
- package/dist/pairing.js.map +0 -7
- package/dist/recipients.d.ts +0 -6
- package/dist/recipients.js +0 -16
- package/dist/recipients.js.map +0 -7
package/dist/append.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StarfishClient } from "./client.js";
|
|
2
|
+
import type { PushSuccess } from "@drakkar.software/starfish-protocol";
|
|
3
|
+
/**
|
|
4
|
+
* Appends `item` to an append-only collection.
|
|
5
|
+
*
|
|
6
|
+
* Sends `{ data: item, baseHash: null }` — the server ignores `baseHash` for
|
|
7
|
+
* append-only collections (conflict detection is disabled or delegated to
|
|
8
|
+
* `checkLastItem` on the server side).
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* await pushAppend(client, "/push/events", { type: "click", ts: Date.now() })
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare function pushAppend(client: StarfishClient, path: string, item: Record<string, unknown>): Promise<PushSuccess>;
|
|
15
|
+
export interface PullAppendListOptions {
|
|
16
|
+
/** Array field name. Defaults to `"items"` (server default). */
|
|
17
|
+
field?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Return only items appended after this timestamp (milliseconds since epoch).
|
|
20
|
+
* Sent as `?checkpoint=<since>`. Omit for a full pull.
|
|
21
|
+
*/
|
|
22
|
+
since?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Return only the last K items. Applied after the `since` filter.
|
|
25
|
+
* Useful for "latest N entries" queries without a tracked checkpoint.
|
|
26
|
+
* Sent as `?last=<K>`.
|
|
27
|
+
*/
|
|
28
|
+
last?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pulls the stored item array from an append-only collection.
|
|
32
|
+
*
|
|
33
|
+
* Returns `data[field]` filtered to an array; returns `[]` when the document
|
|
34
|
+
* does not exist yet or the field is absent / not an array.
|
|
35
|
+
*
|
|
36
|
+
* Pass `{ since: ts }` for incremental pulls — only items appended after `ts`
|
|
37
|
+
* are returned (requires per-item timestamps on the server, available from 2.0.0).
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* // Full pull
|
|
41
|
+
* const events = await pullAppendList(client, "/pull/events")
|
|
42
|
+
*
|
|
43
|
+
* // Incremental pull
|
|
44
|
+
* const newEvents = await pullAppendList(client, "/pull/events", { since: lastSyncTs })
|
|
45
|
+
*
|
|
46
|
+
* // Custom field name
|
|
47
|
+
* const logs = await pullAppendList(client, "/pull/audit", { field: "logs" })
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function pullAppendList<T = unknown>(client: StarfishClient, path: string, options?: PullAppendListOptions): Promise<T[]>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Sync API integration for pending changes.
|
|
3
|
+
* Uses the Web Background Sync API to retry failed sync operations
|
|
4
|
+
* when connectivity is restored, even if the app is closed.
|
|
5
|
+
*/
|
|
6
|
+
/** Check if the Background Sync API is supported in the current environment. */
|
|
7
|
+
export function isBackgroundSyncSupported() {
|
|
8
|
+
return (typeof navigator !== "undefined" &&
|
|
9
|
+
"serviceWorker" in navigator &&
|
|
10
|
+
"SyncManager" in globalThis);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Register a background sync event with the active service worker.
|
|
14
|
+
* Returns true if registration succeeded, false if not supported or no active SW.
|
|
15
|
+
*/
|
|
16
|
+
export async function registerBackgroundSync(opts) {
|
|
17
|
+
if (!isBackgroundSyncSupported())
|
|
18
|
+
return false;
|
|
19
|
+
const tag = opts?.tag ?? "starfish-sync";
|
|
20
|
+
try {
|
|
21
|
+
const registration = await navigator.serviceWorker.ready;
|
|
22
|
+
// @ts-expect-error - SyncManager types may not be available
|
|
23
|
+
await registration.sync.register(tag);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StoreApi } from "zustand/vanilla";
|
|
2
|
+
import type { StarfishStore } from "./zustand.js";
|
|
3
|
+
/**
|
|
4
|
+
* Syncs a Zustand Starfish store across browser tabs using BroadcastChannel.
|
|
5
|
+
* Returns a cleanup function that closes the channel.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setupBroadcastSync(store: StoreApi<StarfishStore>, name: string): () => void;
|
|
8
|
+
/**
|
|
9
|
+
* Syncs a Zustand Starfish store across browser tabs using storage events.
|
|
10
|
+
* Fallback for environments without BroadcastChannel.
|
|
11
|
+
* Returns a cleanup function.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setupStorageFallback(store: StoreApi<StarfishStore>, name: string): () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Auto-detects the best cross-tab sync mechanism and sets it up.
|
|
16
|
+
* Uses BroadcastChannel when available, falls back to storage events.
|
|
17
|
+
* Returns a cleanup function.
|
|
18
|
+
*/
|
|
19
|
+
export declare function setupCrossTabSync(store: StoreApi<StarfishStore>, name: string): () => void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syncs a Zustand Starfish store across browser tabs using BroadcastChannel.
|
|
3
|
+
* Returns a cleanup function that closes the channel.
|
|
4
|
+
*/
|
|
5
|
+
export function setupBroadcastSync(store, name) {
|
|
6
|
+
const channel = new BroadcastChannel(`starfish-${name}`);
|
|
7
|
+
let lastReceivedData = null;
|
|
8
|
+
channel.onmessage = (event) => {
|
|
9
|
+
lastReceivedData = event.data.data;
|
|
10
|
+
store.setState({ data: event.data.data, dirty: event.data.dirty });
|
|
11
|
+
};
|
|
12
|
+
const unsub = store.subscribe((state, prev) => {
|
|
13
|
+
if (state.data === lastReceivedData)
|
|
14
|
+
return;
|
|
15
|
+
if (state.data !== prev.data || state.dirty !== prev.dirty) {
|
|
16
|
+
channel.postMessage({ data: state.data, dirty: state.dirty });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return () => {
|
|
20
|
+
unsub();
|
|
21
|
+
channel.close();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Syncs a Zustand Starfish store across browser tabs using storage events.
|
|
26
|
+
* Fallback for environments without BroadcastChannel.
|
|
27
|
+
* Returns a cleanup function.
|
|
28
|
+
*/
|
|
29
|
+
export function setupStorageFallback(store, name) {
|
|
30
|
+
const storageKey = `starfish-broadcast-${name}`;
|
|
31
|
+
let lastReceivedData = null;
|
|
32
|
+
const onStorage = (e) => {
|
|
33
|
+
if (e.key !== storageKey || !e.newValue)
|
|
34
|
+
return;
|
|
35
|
+
const payload = JSON.parse(e.newValue);
|
|
36
|
+
lastReceivedData = payload.data;
|
|
37
|
+
store.setState({ data: payload.data, dirty: payload.dirty });
|
|
38
|
+
};
|
|
39
|
+
globalThis.addEventListener("storage", onStorage);
|
|
40
|
+
const unsub = store.subscribe((state, prev) => {
|
|
41
|
+
if (state.data === lastReceivedData)
|
|
42
|
+
return;
|
|
43
|
+
if (state.data !== prev.data || state.dirty !== prev.dirty) {
|
|
44
|
+
localStorage.setItem(storageKey, JSON.stringify({ data: state.data, dirty: state.dirty }));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
unsub();
|
|
49
|
+
globalThis.removeEventListener("storage", onStorage);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Auto-detects the best cross-tab sync mechanism and sets it up.
|
|
54
|
+
* Uses BroadcastChannel when available, falls back to storage events.
|
|
55
|
+
* Returns a cleanup function.
|
|
56
|
+
*/
|
|
57
|
+
export function setupCrossTabSync(store, name) {
|
|
58
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
59
|
+
return setupBroadcastSync(store, name);
|
|
60
|
+
}
|
|
61
|
+
if (typeof globalThis.addEventListener === "function" && typeof localStorage !== "undefined") {
|
|
62
|
+
return setupStorageFallback(store, name);
|
|
63
|
+
}
|
|
64
|
+
return () => { };
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StoreApi } from "zustand/vanilla";
|
|
2
|
+
import type { StarfishStore, StarfishState } from "./zustand.js";
|
|
3
|
+
/** Derived sync status for UI display. */
|
|
4
|
+
export type SyncStatus = "synced" | "syncing" | "pending" | "error" | "offline";
|
|
5
|
+
/** Derive a single sync status from store state. */
|
|
6
|
+
export declare function deriveSyncStatus(state: StarfishState): SyncStatus;
|
|
7
|
+
/** Use the full Starfish store state and actions. */
|
|
8
|
+
export declare function useStarfish(store: StoreApi<StarfishStore>): StarfishStore;
|
|
9
|
+
/** Use only the synced data, with an optional selector for fine-grained subscriptions. */
|
|
10
|
+
export declare function useStarfishData<T = Record<string, unknown>>(store: StoreApi<StarfishStore>, selector?: (data: Record<string, unknown>) => T): T;
|
|
11
|
+
/** Use the derived sync status (synced | syncing | pending | error | offline). */
|
|
12
|
+
export declare function useSyncStatus(store: StoreApi<StarfishStore>): SyncStatus;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useStore } from "zustand";
|
|
2
|
+
/** Derive a single sync status from store state. */
|
|
3
|
+
export function deriveSyncStatus(state) {
|
|
4
|
+
if (!state.online)
|
|
5
|
+
return "offline";
|
|
6
|
+
if (state.error)
|
|
7
|
+
return "error";
|
|
8
|
+
if (state.syncing)
|
|
9
|
+
return "syncing";
|
|
10
|
+
if (state.dirty)
|
|
11
|
+
return "pending";
|
|
12
|
+
return "synced";
|
|
13
|
+
}
|
|
14
|
+
/** Use the full Starfish store state and actions. */
|
|
15
|
+
export function useStarfish(store) {
|
|
16
|
+
return useStore(store);
|
|
17
|
+
}
|
|
18
|
+
/** Use only the synced data, with an optional selector for fine-grained subscriptions. */
|
|
19
|
+
export function useStarfishData(store, selector) {
|
|
20
|
+
return useStore(store, (state) => selector ? selector(state.data) : state.data);
|
|
21
|
+
}
|
|
22
|
+
/** Use the derived sync status (synced | syncing | pending | error | offline). */
|
|
23
|
+
export function useSyncStatus(store) {
|
|
24
|
+
return useStore(store, deriveSyncStatus);
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Suspense integration for Starfish sync data.
|
|
3
|
+
* Creates resources that throw Promises while loading (Suspense protocol).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Create a Suspense-compatible resource from an async fetcher.
|
|
7
|
+
* The first call to `read()` triggers the fetch. While loading, `read()` throws
|
|
8
|
+
* a Promise (which React Suspense catches to show a fallback). Once resolved,
|
|
9
|
+
* `read()` returns the value synchronously.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const resource = createSuspenseResource(() => syncManager.pull())
|
|
14
|
+
* function MyComponent() {
|
|
15
|
+
* const data = resource.read() // throws while loading, returns data when ready
|
|
16
|
+
* return <div>{JSON.stringify(data)}</div>
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function createSuspenseResource(fetcher) {
|
|
21
|
+
let status = "pending";
|
|
22
|
+
let result;
|
|
23
|
+
let error;
|
|
24
|
+
let promise = null;
|
|
25
|
+
function init() {
|
|
26
|
+
if (promise)
|
|
27
|
+
return promise;
|
|
28
|
+
promise = fetcher().then((value) => {
|
|
29
|
+
status = "resolved";
|
|
30
|
+
result = value;
|
|
31
|
+
}, (err) => {
|
|
32
|
+
status = "rejected";
|
|
33
|
+
error = err;
|
|
34
|
+
});
|
|
35
|
+
return promise;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
read() {
|
|
39
|
+
switch (status) {
|
|
40
|
+
case "pending":
|
|
41
|
+
throw init();
|
|
42
|
+
case "resolved":
|
|
43
|
+
return result;
|
|
44
|
+
case "rejected":
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/bindings/zustand.js
CHANGED
|
@@ -307,7 +307,7 @@ var StarfishClient = class {
|
|
|
307
307
|
*/
|
|
308
308
|
async buildAuthHeaders(method, pathAndQuery, body) {
|
|
309
309
|
if (this.capProvider) {
|
|
310
|
-
const { cap, devEdPrivHex } = await this.capProvider.getCap();
|
|
310
|
+
const { cap, devEdPrivHex, pubHex } = await this.capProvider.getCap();
|
|
311
311
|
const req = {
|
|
312
312
|
method,
|
|
313
313
|
pathAndQuery,
|
|
@@ -315,12 +315,14 @@ var StarfishClient = class {
|
|
|
315
315
|
host: this.signingHost()
|
|
316
316
|
};
|
|
317
317
|
const { sig, ts, nonce } = await signRequest(req, devEdPrivHex);
|
|
318
|
-
|
|
318
|
+
const headers = {
|
|
319
319
|
Authorization: `Cap ${encodeCapAuth(cap)}`,
|
|
320
320
|
"X-Starfish-Sig": sig,
|
|
321
321
|
"X-Starfish-Ts": String(ts),
|
|
322
322
|
"X-Starfish-Nonce": nonce
|
|
323
323
|
};
|
|
324
|
+
if (pubHex !== void 0) headers["X-Starfish-Pub"] = pubHex;
|
|
325
|
+
return headers;
|
|
324
326
|
}
|
|
325
327
|
return {};
|
|
326
328
|
}
|
|
@@ -402,6 +404,42 @@ var StarfishClient = class {
|
|
|
402
404
|
}
|
|
403
405
|
return res.json();
|
|
404
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Append an element to an appendOnly (`by_timestamp`) collection.
|
|
409
|
+
*
|
|
410
|
+
* Unlike {@link push}, appendOnly writes carry no hash/conflict check — an
|
|
411
|
+
* authorized append is always accepted. Each element is stored server-side as
|
|
412
|
+
* `{ts, data}` and pulls can filter by `ts` via `since`/`checkpoint`.
|
|
413
|
+
*
|
|
414
|
+
* @param path - the push endpoint (e.g. "/push/events")
|
|
415
|
+
* @param data - the element payload. For a `delegated` collection, encrypt it
|
|
416
|
+
* first (e.g. `createKeyringEncryptor(keyring, kem).encrypt(data)`); the
|
|
417
|
+
* server stores it opaquely and never reads it.
|
|
418
|
+
* @param opts.ts - optional client-supplied element timestamp (ms). Must be a
|
|
419
|
+
* non-negative integer strictly greater than the latest stored element's ts
|
|
420
|
+
* (else the server responds 409). Omit to let the server assign one.
|
|
421
|
+
* @throws {StarfishHttpError} on a non-2xx response (e.g. 409 for a
|
|
422
|
+
* non-monotonic timestamp).
|
|
423
|
+
*/
|
|
424
|
+
async append(path, data, opts = {}) {
|
|
425
|
+
const bodyObj = { data };
|
|
426
|
+
if (opts.ts !== void 0) bodyObj["ts"] = opts.ts;
|
|
427
|
+
const body = JSON.stringify(bodyObj);
|
|
428
|
+
const authHeaders = await this.buildAuthHeaders("POST", path, body);
|
|
429
|
+
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: {
|
|
432
|
+
"Content-Type": "application/json",
|
|
433
|
+
Accept: "application/json",
|
|
434
|
+
...authHeaders
|
|
435
|
+
},
|
|
436
|
+
body
|
|
437
|
+
});
|
|
438
|
+
if (!res.ok) {
|
|
439
|
+
throw new StarfishHttpError(res.status, await res.text());
|
|
440
|
+
}
|
|
441
|
+
return res.json();
|
|
442
|
+
}
|
|
405
443
|
/**
|
|
406
444
|
* Pull binary data from a blob collection.
|
|
407
445
|
* Returns raw bytes with the content hash from the ETag header.
|