@drakkar.software/starfish-client 3.0.0-alpha.2 → 3.0.0-alpha.5
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/_crypto_helpers.d.ts +4 -0
- package/dist/bindings/zustand.js +37 -12
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/cap-mint.d.ts +20 -0
- package/dist/cap-mint.js +12 -0
- package/dist/cap-mint.js.map +7 -0
- package/dist/client.d.ts +13 -0
- package/dist/directory.d.ts +9 -0
- package/dist/directory.js +24 -0
- package/dist/directory.js.map +7 -0
- package/dist/identity.d.ts +4 -82
- package/dist/identity.js +2 -354
- package/dist/identity.js.map +4 -4
- package/dist/index.js +37 -12
- package/dist/index.js.map +2 -2
- package/dist/keyring.d.ts +6 -0
- package/dist/keyring.js +26 -0
- package/dist/keyring.js.map +7 -0
- package/dist/pairing.d.ts +6 -0
- package/dist/pairing.js +26 -0
- package/dist/pairing.js.map +7 -0
- package/dist/recipients.d.ts +6 -0
- package/dist/recipients.js +16 -0
- package/dist/recipients.js.map +7 -0
- package/dist/types.d.ts +28 -1
- package/package.json +2 -2
- package/dist/append.d.ts +0 -50
- package/dist/background-sync.js +0 -29
- package/dist/bindings/broadcast.d.ts +0 -19
- package/dist/bindings/broadcast.js +0 -65
- package/dist/bindings/react.d.ts +0 -12
- package/dist/bindings/react.js +0 -25
- package/dist/bindings/suspense.js +0 -49
- package/dist/client.js +0 -112
- package/dist/config.js +0 -18
- package/dist/crypto.js +0 -49
- package/dist/debounced-sync.js +0 -120
- package/dist/dedup.js +0 -35
- package/dist/entitlements.js +0 -41
- package/dist/export.js +0 -115
- package/dist/group-crypto.d.ts +0 -111
- package/dist/group-crypto.js +0 -205
- package/dist/group-crypto.js.map +0 -7
- package/dist/history.js +0 -61
- package/dist/logger.js +0 -80
- package/dist/migrate.js +0 -38
- package/dist/mobile-lifecycle.js +0 -55
- package/dist/multi-store.js +0 -92
- package/dist/polling.js +0 -52
- package/dist/resolvers.js +0 -223
- package/dist/service-worker.js +0 -55
- package/dist/storage/indexeddb.js +0 -59
- package/dist/sync.js +0 -127
- package/dist/types.js +0 -18
- package/dist/validate.js +0 -28
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-2 transitional shim. The implementation lives in
|
|
3
|
+
* `@drakkar.software/starfish-keyring`. This file is removed in Phase 3.
|
|
4
|
+
*/
|
|
5
|
+
export { KEYRING_WRAP_SALT, KEYRING_WRAP_INFO, KEYRING_IV_BYTES, wrapForRecipient, unwrapFromEntry, verifyEntrySignature, createKeyring, addRecipient, rotateEpoch, createKeyringEncryptor, } from "@drakkar.software/starfish-keyring";
|
|
6
|
+
export type { WrappedKeyEntry, KeyringEpoch, Keyring, KeyringEncryptor, } from "@drakkar.software/starfish-keyring";
|
package/dist/keyring.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/keyring.ts
|
|
2
|
+
import {
|
|
3
|
+
KEYRING_WRAP_SALT,
|
|
4
|
+
KEYRING_WRAP_INFO,
|
|
5
|
+
KEYRING_IV_BYTES,
|
|
6
|
+
wrapForRecipient,
|
|
7
|
+
unwrapFromEntry,
|
|
8
|
+
verifyEntrySignature,
|
|
9
|
+
createKeyring,
|
|
10
|
+
addRecipient,
|
|
11
|
+
rotateEpoch,
|
|
12
|
+
createKeyringEncryptor
|
|
13
|
+
} from "@drakkar.software/starfish-keyring";
|
|
14
|
+
export {
|
|
15
|
+
KEYRING_IV_BYTES,
|
|
16
|
+
KEYRING_WRAP_INFO,
|
|
17
|
+
KEYRING_WRAP_SALT,
|
|
18
|
+
addRecipient,
|
|
19
|
+
createKeyring,
|
|
20
|
+
createKeyringEncryptor,
|
|
21
|
+
rotateEpoch,
|
|
22
|
+
unwrapFromEntry,
|
|
23
|
+
verifyEntrySignature,
|
|
24
|
+
wrapForRecipient
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=keyring.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/keyring.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Phase-2 transitional shim. The implementation lives in\n * `@drakkar.software/starfish-keyring`. This file is removed in Phase 3.\n */\nexport {\n KEYRING_WRAP_SALT,\n KEYRING_WRAP_INFO,\n KEYRING_IV_BYTES,\n wrapForRecipient,\n unwrapFromEntry,\n verifyEntrySignature,\n createKeyring,\n addRecipient,\n rotateEpoch,\n createKeyringEncryptor,\n} from \"@drakkar.software/starfish-keyring\"\nexport type {\n WrappedKeyEntry,\n KeyringEpoch,\n Keyring,\n KeyringEncryptor,\n} from \"@drakkar.software/starfish-keyring\"\n"],
|
|
5
|
+
"mappings": ";AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-2 transitional shim. The implementation lives in
|
|
3
|
+
* `@drakkar.software/starfish-identities`. Removed in Phase 3.
|
|
4
|
+
*/
|
|
5
|
+
export { bootstrapRootIdentity, buildPairingQr, parsePairingQr, assemblePairingBundle, installPairingBundle, deriveCodeKey, buildPairingRequest, readPairingRequest, buildPairingResponse, readPairingResponse, } from "@drakkar.software/starfish-identities";
|
|
6
|
+
export type { DeviceCredentials, PairingQrPayload, PairingBundle, WrappedCekEntry, InstalledPairingResult, AssemblePairingBundleOpts, PairingRequestEncrypted, PairingResponseEncrypted, } from "@drakkar.software/starfish-identities";
|
package/dist/pairing.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/pairing.ts
|
|
2
|
+
import {
|
|
3
|
+
bootstrapRootIdentity,
|
|
4
|
+
buildPairingQr,
|
|
5
|
+
parsePairingQr,
|
|
6
|
+
assemblePairingBundle,
|
|
7
|
+
installPairingBundle,
|
|
8
|
+
deriveCodeKey,
|
|
9
|
+
buildPairingRequest,
|
|
10
|
+
readPairingRequest,
|
|
11
|
+
buildPairingResponse,
|
|
12
|
+
readPairingResponse
|
|
13
|
+
} from "@drakkar.software/starfish-identities";
|
|
14
|
+
export {
|
|
15
|
+
assemblePairingBundle,
|
|
16
|
+
bootstrapRootIdentity,
|
|
17
|
+
buildPairingQr,
|
|
18
|
+
buildPairingRequest,
|
|
19
|
+
buildPairingResponse,
|
|
20
|
+
deriveCodeKey,
|
|
21
|
+
installPairingBundle,
|
|
22
|
+
parsePairingQr,
|
|
23
|
+
readPairingRequest,
|
|
24
|
+
readPairingResponse
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=pairing.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/pairing.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Phase-2 transitional shim. The implementation lives in\n * `@drakkar.software/starfish-identities`. Removed in Phase 3.\n */\nexport {\n bootstrapRootIdentity,\n buildPairingQr,\n parsePairingQr,\n assemblePairingBundle,\n installPairingBundle,\n deriveCodeKey,\n buildPairingRequest,\n readPairingRequest,\n buildPairingResponse,\n readPairingResponse,\n} from \"@drakkar.software/starfish-identities\"\nexport type {\n DeviceCredentials,\n PairingQrPayload,\n PairingBundle,\n WrappedCekEntry,\n InstalledPairingResult,\n AssemblePairingBundleOpts,\n PairingRequestEncrypted,\n PairingResponseEncrypted,\n} from \"@drakkar.software/starfish-identities\"\n"],
|
|
5
|
+
"mappings": ";AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-2 transitional shim. The implementation lives in
|
|
3
|
+
* `@drakkar.software/starfish-keyring`. This file is removed in Phase 3.
|
|
4
|
+
*/
|
|
5
|
+
export { keyringPathFor, addCollectionRecipient, removeRecipient, listRecipients, currentEpoch, } from "@drakkar.software/starfish-keyring";
|
|
6
|
+
export type { RecipientRef, AdderKeys, ListedRecipient, } from "@drakkar.software/starfish-keyring";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/recipients.ts
|
|
2
|
+
import {
|
|
3
|
+
keyringPathFor,
|
|
4
|
+
addCollectionRecipient,
|
|
5
|
+
removeRecipient,
|
|
6
|
+
listRecipients,
|
|
7
|
+
currentEpoch
|
|
8
|
+
} from "@drakkar.software/starfish-keyring";
|
|
9
|
+
export {
|
|
10
|
+
addCollectionRecipient,
|
|
11
|
+
currentEpoch,
|
|
12
|
+
keyringPathFor,
|
|
13
|
+
listRecipients,
|
|
14
|
+
removeRecipient
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=recipients.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/recipients.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Phase-2 transitional shim. The implementation lives in\n * `@drakkar.software/starfish-keyring`. This file is removed in Phase 3.\n */\nexport {\n keyringPathFor,\n addCollectionRecipient,\n removeRecipient,\n listRecipients,\n currentEpoch,\n} from \"@drakkar.software/starfish-keyring\"\nexport type {\n RecipientRef,\n AdderKeys,\n ListedRecipient,\n} from \"@drakkar.software/starfish-keyring\"\n"],
|
|
5
|
+
"mappings": ";AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CapCert } from "@drakkar.software/starfish-protocol";
|
|
1
|
+
import type { Alg, CapCert } from "@drakkar.software/starfish-protocol";
|
|
2
2
|
/** Push conflict error (HTTP 409). */
|
|
3
3
|
export declare class ConflictError extends Error {
|
|
4
4
|
constructor();
|
|
@@ -29,17 +29,44 @@ export interface StarfishCapProvider {
|
|
|
29
29
|
* The client then sends it as `X-Starfish-Pub` so the server can verify the
|
|
30
30
|
* request signature against it and check the cap's `aud` allow-list. Omit
|
|
31
31
|
* `pubHex` for device/member caps (the server uses `cap.sub`).
|
|
32
|
+
*
|
|
33
|
+
* `presenterAlg` is the crypto suite of `devEdPrivHex` (the key that signs
|
|
34
|
+
* the request). It matters only for `audience` caps, where the presenter is
|
|
35
|
+
* an arbitrary redeemer whose suite is unrelated to the cap's `issAlg`; the
|
|
36
|
+
* client sends it as `X-Starfish-Alg`. For device/member caps the subject's
|
|
37
|
+
* suite is taken authoritatively from the verified cert, so this is ignored.
|
|
38
|
+
* Defaults to `"ed25519"` when omitted.
|
|
32
39
|
*/
|
|
33
40
|
getCap(): Promise<{
|
|
34
41
|
cap: CapCert;
|
|
35
42
|
devEdPrivHex: string;
|
|
36
43
|
pubHex?: string;
|
|
44
|
+
presenterAlg?: Alg;
|
|
37
45
|
}>;
|
|
38
46
|
}
|
|
39
47
|
/** Options for creating a StarfishClient. */
|
|
40
48
|
export interface StarfishClientOptions {
|
|
41
49
|
/** Base URL of the Starfish server (e.g. "https://api.example.com/v1"). */
|
|
42
50
|
baseUrl: string;
|
|
51
|
+
/**
|
|
52
|
+
* Optional namespace for a namespace-mounted server. When set, every request
|
|
53
|
+
* path `/{action}/…` is rewritten to `/v1/{namespace}/{action}/…` for BOTH the
|
|
54
|
+
* URL the client hits AND the canonical path it signs, so the signature the
|
|
55
|
+
* server reconstructs from the namespaced URL verifies (no rewrite layer
|
|
56
|
+
* needed). Mirrors the Python client's `namespace` parameter.
|
|
57
|
+
*
|
|
58
|
+
* Crucially this also rewrites the paths that namespace-unaware SDK helpers
|
|
59
|
+
* build internally (e.g. `starfish-keyring`'s `addCollectionRecipient`, blob
|
|
60
|
+
* uploads), so consumers no longer hand-prefix paths or wrap the client to
|
|
61
|
+
* reach a namespaced deployment. Leave unset (default) for a root-mounted
|
|
62
|
+
* server — paths pass through unchanged, byte-identical to before.
|
|
63
|
+
*
|
|
64
|
+
* Pass the bare namespace name (e.g. `"octochat"`); `baseUrl` then carries only
|
|
65
|
+
* the origin (and any reverse-proxy mount the proxy strips), not the `/v1`
|
|
66
|
+
* version segment. Must match `[A-Za-z0-9_-]+` and not be a reserved route name
|
|
67
|
+
* (`pull`, `push`, `health`, `batch`).
|
|
68
|
+
*/
|
|
69
|
+
namespace?: string;
|
|
43
70
|
/**
|
|
44
71
|
* Cap-cert provider. When set, requests are signed with Ed25519 and carry
|
|
45
72
|
* `Authorization: Cap <…>`. Omit for unauthenticated public-read collections.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakkar.software/starfish-client",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/Drakkar-Software/starfish.git",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@drakkar.software/starfish-protocol": "3.0.0-alpha.
|
|
63
|
+
"@drakkar.software/starfish-protocol": "3.0.0-alpha.5"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@legendapp/state": "^2.0.0",
|
package/dist/append.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
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[]>;
|
package/dist/background-sync.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
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;
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/bindings/react.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
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;
|
package/dist/bindings/react.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
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/client.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { ConflictError, StarfishHttpError } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Low-level HTTP client for the Starfish sync protocol.
|
|
4
|
-
* Handles auth headers and response parsing.
|
|
5
|
-
*/
|
|
6
|
-
export class StarfishClient {
|
|
7
|
-
baseUrl;
|
|
8
|
-
auth;
|
|
9
|
-
fetch;
|
|
10
|
-
constructor(options) {
|
|
11
|
-
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
12
|
-
this.auth = options.auth;
|
|
13
|
-
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Pull synced data from the server.
|
|
17
|
-
* @param path - The pull endpoint path (e.g. "/pull/users/abc/settings")
|
|
18
|
-
* @param checkpoint - Only return data updated after this timestamp (0 = full pull)
|
|
19
|
-
*/
|
|
20
|
-
async pull(path, checkpoint) {
|
|
21
|
-
const url = checkpoint
|
|
22
|
-
? `${this.baseUrl}${path}?checkpoint=${checkpoint}`
|
|
23
|
-
: `${this.baseUrl}${path}`;
|
|
24
|
-
const authHeaders = this.auth
|
|
25
|
-
? await this.auth({ method: "GET", path, body: null })
|
|
26
|
-
: {};
|
|
27
|
-
const res = await this.fetch(url, {
|
|
28
|
-
method: "GET",
|
|
29
|
-
headers: { Accept: "application/json", ...authHeaders },
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
throw new StarfishHttpError(res.status, await res.text());
|
|
33
|
-
}
|
|
34
|
-
return res.json();
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Push synced data to the server.
|
|
38
|
-
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
39
|
-
* @param data - The full document data to push
|
|
40
|
-
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
41
|
-
* @param authorSignature - Optional author signature for provenance
|
|
42
|
-
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
43
|
-
*/
|
|
44
|
-
async push(path, data, baseHash, authorSignature) {
|
|
45
|
-
const body = JSON.stringify({
|
|
46
|
-
data,
|
|
47
|
-
baseHash,
|
|
48
|
-
...(authorSignature && { authorSignature }),
|
|
49
|
-
});
|
|
50
|
-
const authHeaders = this.auth
|
|
51
|
-
? await this.auth({ method: "POST", path, body })
|
|
52
|
-
: {};
|
|
53
|
-
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: {
|
|
56
|
-
"Content-Type": "application/json",
|
|
57
|
-
Accept: "application/json",
|
|
58
|
-
...authHeaders,
|
|
59
|
-
},
|
|
60
|
-
body,
|
|
61
|
-
});
|
|
62
|
-
if (res.status === 409) {
|
|
63
|
-
throw new ConflictError();
|
|
64
|
-
}
|
|
65
|
-
if (!res.ok) {
|
|
66
|
-
throw new StarfishHttpError(res.status, await res.text());
|
|
67
|
-
}
|
|
68
|
-
return res.json();
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Pull binary data from a blob collection.
|
|
72
|
-
* Returns raw bytes with the content hash from the ETag header.
|
|
73
|
-
*/
|
|
74
|
-
async pullBlob(path) {
|
|
75
|
-
const authHeaders = this.auth
|
|
76
|
-
? await this.auth({ method: "GET", path, body: null })
|
|
77
|
-
: {};
|
|
78
|
-
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
79
|
-
method: "GET",
|
|
80
|
-
headers: { Accept: "*/*", ...authHeaders },
|
|
81
|
-
});
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
throw new StarfishHttpError(res.status, await res.text());
|
|
84
|
-
}
|
|
85
|
-
const etag = res.headers.get("ETag")?.replace(/"/g, "") ?? null;
|
|
86
|
-
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
87
|
-
const data = await res.arrayBuffer();
|
|
88
|
-
return { data, hash: etag, contentType };
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Push binary data to a blob collection.
|
|
92
|
-
* Binary collections use last-write-wins (no conflict detection).
|
|
93
|
-
*/
|
|
94
|
-
async pushBlob(path, data, contentType) {
|
|
95
|
-
const authHeaders = this.auth
|
|
96
|
-
? await this.auth({ method: "POST", path, body: null })
|
|
97
|
-
: {};
|
|
98
|
-
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: {
|
|
101
|
-
"Content-Type": contentType,
|
|
102
|
-
Accept: "application/json",
|
|
103
|
-
...authHeaders,
|
|
104
|
-
},
|
|
105
|
-
body: data,
|
|
106
|
-
});
|
|
107
|
-
if (!res.ok) {
|
|
108
|
-
throw new StarfishHttpError(res.status, await res.text());
|
|
109
|
-
}
|
|
110
|
-
return res.json();
|
|
111
|
-
}
|
|
112
|
-
}
|
package/dist/config.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fetch the server's collection manifest from GET /config.
|
|
3
|
-
*
|
|
4
|
-
* @param baseUrl - Base URL of the Starfish server (e.g. `"https://api.example.com/v1"`).
|
|
5
|
-
* @param options.headers - Optional request headers (e.g. `Authorization`).
|
|
6
|
-
* @throws {Error} if the server returns a non-2xx response.
|
|
7
|
-
*/
|
|
8
|
-
export async function fetchServerConfig(baseUrl, options) {
|
|
9
|
-
const url = `${baseUrl.replace(/\/$/, "")}/config`;
|
|
10
|
-
const res = await fetch(url, {
|
|
11
|
-
method: "GET",
|
|
12
|
-
headers: options?.headers,
|
|
13
|
-
});
|
|
14
|
-
if (!res.ok) {
|
|
15
|
-
throw new Error(`fetchServerConfig: ${res.status} ${res.statusText}`);
|
|
16
|
-
}
|
|
17
|
-
return res.json();
|
|
18
|
-
}
|
package/dist/crypto.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { getCrypto, getBase64, IV_BYTES, ENCRYPTED_KEY, deriveKey } from "@drakkar.software/starfish-protocol";
|
|
2
|
-
const ALGO = "AES-GCM";
|
|
3
|
-
export { ENCRYPTED_KEY };
|
|
4
|
-
/**
|
|
5
|
-
* Creates an Encryptor that uses AES-256-GCM with HKDF-derived keys.
|
|
6
|
-
*/
|
|
7
|
-
export function createEncryptor(secret, salt, info = "starfish-e2e") {
|
|
8
|
-
if (!secret)
|
|
9
|
-
throw new Error("encryptionSecret must not be empty");
|
|
10
|
-
if (!salt)
|
|
11
|
-
throw new Error("encryptionSalt must not be empty");
|
|
12
|
-
const keyPromise = deriveKey(secret, salt, info);
|
|
13
|
-
return {
|
|
14
|
-
async encrypt(data) {
|
|
15
|
-
const key = await keyPromise;
|
|
16
|
-
const c = getCrypto();
|
|
17
|
-
const b64 = getBase64();
|
|
18
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
19
|
-
const iv = c.getRandomValues(new Uint8Array(IV_BYTES));
|
|
20
|
-
const ciphertext = await c.subtle.encrypt({ name: ALGO, iv }, key, plaintext);
|
|
21
|
-
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
22
|
-
combined.set(iv);
|
|
23
|
-
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
24
|
-
return { [ENCRYPTED_KEY]: b64.encode(combined) };
|
|
25
|
-
},
|
|
26
|
-
async decrypt(wrapper) {
|
|
27
|
-
const encoded = wrapper[ENCRYPTED_KEY];
|
|
28
|
-
if (typeof encoded !== "string") {
|
|
29
|
-
throw new Error("Expected encrypted data but received unencrypted document");
|
|
30
|
-
}
|
|
31
|
-
const key = await keyPromise;
|
|
32
|
-
const c = getCrypto();
|
|
33
|
-
const b64 = getBase64();
|
|
34
|
-
const combined = b64.decode(encoded);
|
|
35
|
-
if (combined.length < IV_BYTES) {
|
|
36
|
-
throw new Error("Encrypted data is too short");
|
|
37
|
-
}
|
|
38
|
-
const iv = combined.slice(0, IV_BYTES);
|
|
39
|
-
const ciphertext = combined.slice(IV_BYTES);
|
|
40
|
-
try {
|
|
41
|
-
const plaintext = await c.subtle.decrypt({ name: ALGO, iv }, key, ciphertext);
|
|
42
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
throw new Error("Decryption failed: data may be tampered or key is incorrect", { cause: err });
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|