@drakkar.software/starfish-client 2.3.0 → 3.0.0-alpha.0
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 +219 -0
- package/dist/_crypto_helpers.d.ts +4 -0
- package/dist/bindings/zustand.d.ts +5 -4
- package/dist/bindings/zustand.js +125 -79
- package/dist/bindings/zustand.js.map +4 -4
- 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 +52 -3
- package/dist/config.d.ts +1 -4
- 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.d.ts +8 -10
- package/dist/index.js +131 -251
- package/dist/index.js.map +4 -4
- 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/sync.d.ts +32 -8
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +2 -2
- package/dist/types.d.ts +48 -9
- package/package.json +3 -12
- package/dist/background-sync.js +0 -29
- 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/hash.d.ts +0 -10
- package/dist/hash.js +0 -34
- 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/platform.d.ts +0 -52
- package/dist/platform.js +0 -62
- 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/sync.d.ts
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import type { PullResult } from "@drakkar.software/starfish-protocol";
|
|
2
2
|
import type { ConflictResolver } from "./types.js";
|
|
3
|
+
import type { Encryptor } from "@drakkar.software/starfish-protocol";
|
|
3
4
|
import { StarfishClient } from "./client.js";
|
|
4
|
-
import type { Encryptor } from "./crypto.js";
|
|
5
5
|
import type { SyncLogger } from "./logger.js";
|
|
6
6
|
import type { Validator } from "./validate.js";
|
|
7
7
|
export declare class AbortError extends Error {
|
|
8
8
|
constructor();
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* v3.0 author-signature plumbing for `SyncManager`.
|
|
12
|
+
*
|
|
13
|
+
* Returns the device's Ed25519 public key (hex) and a function that signs
|
|
14
|
+
* arbitrary payload bytes. `SyncManager` calls `getSigner()` once per push
|
|
15
|
+
* and uses the returned `sign` to produce a base64-encoded signature over
|
|
16
|
+
* the canonical stringification of the encrypted payload (sans author fields).
|
|
17
|
+
*
|
|
18
|
+
* Implementations typically wrap the same Ed25519 private key used by
|
|
19
|
+
* `StarfishCapProvider` so that `cap.sub === devEdPubHex`.
|
|
20
|
+
*/
|
|
21
|
+
export interface SyncSigner {
|
|
22
|
+
/**
|
|
23
|
+
* Returns the device's `cap.sub` (Ed25519 pubkey, hex) and a payload signer.
|
|
24
|
+
* The `sign` function receives the canonical signing input bytes and must
|
|
25
|
+
* return the raw 64-byte Ed25519 signature.
|
|
26
|
+
*/
|
|
27
|
+
getSigner(): Promise<{
|
|
28
|
+
devEdPubHex: string;
|
|
29
|
+
sign(payload: Uint8Array): Promise<Uint8Array>;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
10
32
|
export interface SyncManagerOptions {
|
|
11
33
|
client: StarfishClient;
|
|
12
34
|
pullPath: string;
|
|
@@ -15,15 +37,17 @@ export interface SyncManagerOptions {
|
|
|
15
37
|
onConflict?: ConflictResolver;
|
|
16
38
|
/** Max conflict retry attempts (default: 3). */
|
|
17
39
|
maxRetries?: number;
|
|
18
|
-
encryptionSecret?: string;
|
|
19
|
-
encryptionSalt?: string;
|
|
20
|
-
encryptionInfo?: string;
|
|
21
40
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
41
|
+
* Encryptor for client-side E2E encryption. For v3 `delegated` collections,
|
|
42
|
+
* build it via `createKeyringEncryptor(keyring, deviceKemKeys)`.
|
|
24
43
|
*/
|
|
25
44
|
encryptor?: Encryptor;
|
|
26
|
-
|
|
45
|
+
/**
|
|
46
|
+
* v3 author-signature plumbing. When set, every push attaches
|
|
47
|
+
* `authorPubkey` (= `cap.sub`) and `authorSignature` (= base64 Ed25519 over
|
|
48
|
+
* stable-stringify of the encrypted payload minus author fields).
|
|
49
|
+
*/
|
|
50
|
+
signer?: SyncSigner;
|
|
27
51
|
/** Structured logger for sync events. */
|
|
28
52
|
logger?: SyncLogger;
|
|
29
53
|
/** Name passed to logger methods (default: derived from pullPath). */
|
|
@@ -38,7 +62,7 @@ export declare class SyncManager {
|
|
|
38
62
|
private readonly onConflict;
|
|
39
63
|
private readonly maxRetries;
|
|
40
64
|
private readonly encryptor;
|
|
41
|
-
private readonly
|
|
65
|
+
private readonly signer?;
|
|
42
66
|
private readonly logger?;
|
|
43
67
|
private readonly loggerName;
|
|
44
68
|
private readonly validate?;
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PullResult, PushSuccess } from "@drakkar.software/starfish-protocol";
|
|
2
2
|
import { StarfishClient } from "./client.js";
|
|
3
3
|
type PullFn = (path: string, checkpoint?: number) => Promise<PullResult>;
|
|
4
|
-
type PushFn = (path: string, data: Record<string, unknown>, baseHash: string | null
|
|
4
|
+
type PushFn = (path: string, data: Record<string, unknown>, baseHash: string | null) => Promise<PushSuccess>;
|
|
5
5
|
/**
|
|
6
6
|
* Creates a mock StarfishClient for testing.
|
|
7
7
|
* Override individual methods or use the defaults (returns static data).
|
package/dist/testing.js
CHANGED
|
@@ -16,9 +16,9 @@ function createMockClient(overrides) {
|
|
|
16
16
|
pullCalls.push({ path, checkpoint });
|
|
17
17
|
return pull(path, checkpoint);
|
|
18
18
|
},
|
|
19
|
-
push: async (path, data, baseHash
|
|
19
|
+
push: async (path, data, baseHash) => {
|
|
20
20
|
pushCalls.push({ path, data, baseHash });
|
|
21
|
-
return push(path, data, baseHash
|
|
21
|
+
return push(path, data, baseHash);
|
|
22
22
|
},
|
|
23
23
|
pullCalls,
|
|
24
24
|
pushCalls
|
package/dist/testing.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/testing.ts"],
|
|
4
|
-
"sourcesContent": ["import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport { StarfishClient } from \"./client.js\"\n\ntype PullFn = (path: string, checkpoint?: number) => Promise<PullResult>\ntype PushFn = (path: string, data: Record<string, unknown>, baseHash: string | null
|
|
5
|
-
"mappings": ";AAkBO,SAAS,iBAAiB,WAG4I;AAC3K,QAAM,YAA0D,CAAC;AACjE,QAAM,YAA6F,CAAC;AAEpG,QAAM,OAAe,WAAW,SAAS,aAAa;AAAA,IACpD,MAAM,CAAC;AAAA,IACP,MAAM;AAAA,IACN,WAAW,KAAK,IAAI;AAAA,EACtB;AAEA,QAAM,OAAe,WAAW,SAAS,aAAa;AAAA,IACpD,MAAM;AAAA,IACN,WAAW,KAAK,IAAI;AAAA,EACtB;AAEA,SAAO;AAAA,IACL,MAAM,OAAO,MAAc,eAAwB;AACjD,gBAAU,KAAK,EAAE,MAAM,WAAW,CAAC;AACnC,aAAO,KAAK,MAAM,UAAU;AAAA,IAC9B;AAAA,IACA,MAAM,OAAO,MAAc,MAA+B,
|
|
4
|
+
"sourcesContent": ["import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport { StarfishClient } from \"./client.js\"\n\ntype PullFn = (path: string, checkpoint?: number) => Promise<PullResult>\ntype PushFn = (path: string, data: Record<string, unknown>, baseHash: string | null) => Promise<PushSuccess>\n\n/**\n * Creates a mock StarfishClient for testing.\n * Override individual methods or use the defaults (returns static data).\n *\n * @example\n * ```ts\n * const client = createMockClient({\n * pull: async () => ({ data: { key: \"value\" }, hash: \"h1\", timestamp: 100 }),\n * })\n * const sync = new SyncManager({ client, pullPath: \"/pull/test\", pushPath: \"/push/test\" })\n * ```\n */\nexport function createMockClient(overrides?: {\n pull?: PullFn\n push?: PushFn\n}): StarfishClient & { pullCalls: Array<{ path: string; checkpoint?: number }>; pushCalls: Array<{ path: string; data: Record<string, unknown>; baseHash: string | null }> } {\n const pullCalls: Array<{ path: string; checkpoint?: number }> = []\n const pushCalls: Array<{ path: string; data: Record<string, unknown>; baseHash: string | null }> = []\n\n const pull: PullFn = overrides?.pull ?? (async () => ({\n data: {},\n hash: \"mock-hash\",\n timestamp: Date.now(),\n }))\n\n const push: PushFn = overrides?.push ?? (async () => ({\n hash: \"mock-push-hash\",\n timestamp: Date.now(),\n }))\n\n return {\n pull: async (path: string, checkpoint?: number) => {\n pullCalls.push({ path, checkpoint })\n return pull(path, checkpoint)\n },\n push: async (path: string, data: Record<string, unknown>, baseHash: string | null) => {\n pushCalls.push({ path, data, baseHash })\n return push(path, data, baseHash)\n },\n pullCalls,\n pushCalls,\n } as unknown as StarfishClient & { pullCalls: typeof pullCalls; pushCalls: typeof pushCalls }\n}\n\n/**\n * Creates a mock fetch that returns predefined responses.\n * Useful for testing StarfishClient directly.\n *\n * @example\n * ```ts\n * const fetch = createMockFetch({ data: { key: \"value\" }, hash: \"h1\", timestamp: 100 })\n * const client = new StarfishClient({ baseUrl: \"https://example.com\", fetch })\n * ```\n */\nexport function createMockFetch(\n pullResponse?: PullResult,\n pushResponse?: PushSuccess,\n): typeof globalThis.fetch {\n return async (input) => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : (input as Request).url\n if (url.includes(\"/pull/\")) {\n return new Response(JSON.stringify(pullResponse ?? { data: {}, hash: \"h\", timestamp: 1 }), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n })\n }\n return new Response(JSON.stringify(pushResponse ?? { hash: \"h\", timestamp: 1 }), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n })\n }\n}\n\n/**\n * Creates a mock fetch that simulates a conflict (409) on the first push,\n * then succeeds on retry. Useful for testing conflict resolution.\n */\nexport function createConflictFetch(\n conflictPullResponse: PullResult,\n successPushResponse?: PushSuccess,\n): typeof globalThis.fetch {\n let pushCount = 0\n return async (input, init?) => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : (input as Request).url\n if (url.includes(\"/pull/\")) {\n return new Response(JSON.stringify(conflictPullResponse), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n })\n }\n pushCount++\n if (pushCount === 1) {\n return new Response(JSON.stringify({ error: \"hash_mismatch\" }), { status: 409 })\n }\n return new Response(JSON.stringify(successPushResponse ?? { hash: \"resolved\", timestamp: Date.now() }), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n })\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAkBO,SAAS,iBAAiB,WAG4I;AAC3K,QAAM,YAA0D,CAAC;AACjE,QAAM,YAA6F,CAAC;AAEpG,QAAM,OAAe,WAAW,SAAS,aAAa;AAAA,IACpD,MAAM,CAAC;AAAA,IACP,MAAM;AAAA,IACN,WAAW,KAAK,IAAI;AAAA,EACtB;AAEA,QAAM,OAAe,WAAW,SAAS,aAAa;AAAA,IACpD,MAAM;AAAA,IACN,WAAW,KAAK,IAAI;AAAA,EACtB;AAEA,SAAO;AAAA,IACL,MAAM,OAAO,MAAc,eAAwB;AACjD,gBAAU,KAAK,EAAE,MAAM,WAAW,CAAC;AACnC,aAAO,KAAK,MAAM,UAAU;AAAA,IAC9B;AAAA,IACA,MAAM,OAAO,MAAc,MAA+B,aAA4B;AACpF,gBAAU,KAAK,EAAE,MAAM,MAAM,SAAS,CAAC;AACvC,aAAO,KAAK,MAAM,MAAM,QAAQ;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAYO,SAAS,gBACd,cACA,cACyB;AACzB,SAAO,OAAO,UAAU;AACtB,UAAM,MAAM,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAQ,MAAkB;AACvG,QAAI,IAAI,SAAS,QAAQ,GAAG;AAC1B,aAAO,IAAI,SAAS,KAAK,UAAU,gBAAgB,EAAE,MAAM,CAAC,GAAG,MAAM,KAAK,WAAW,EAAE,CAAC,GAAG;AAAA,QACzF,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAChD,CAAC;AAAA,IACH;AACA,WAAO,IAAI,SAAS,KAAK,UAAU,gBAAgB,EAAE,MAAM,KAAK,WAAW,EAAE,CAAC,GAAG;AAAA,MAC/E,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAChD,CAAC;AAAA,EACH;AACF;AAMO,SAAS,oBACd,sBACA,qBACyB;AACzB,MAAI,YAAY;AAChB,SAAO,OAAO,OAAO,SAAU;AAC7B,UAAM,MAAM,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAQ,MAAkB;AACvG,QAAI,IAAI,SAAS,QAAQ,GAAG;AAC1B,aAAO,IAAI,SAAS,KAAK,UAAU,oBAAoB,GAAG;AAAA,QACxD,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAChD,CAAC;AAAA,IACH;AACA;AACA,QAAI,cAAc,GAAG;AACnB,aAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,gBAAgB,CAAC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACjF;AACA,WAAO,IAAI,SAAS,KAAK,UAAU,uBAAuB,EAAE,MAAM,YAAY,WAAW,KAAK,IAAI,EAAE,CAAC,GAAG;AAAA,MACtG,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAChD,CAAC;AAAA,EACH;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CapCert } from "@drakkar.software/starfish-protocol";
|
|
1
2
|
/** Push conflict error (HTTP 409). */
|
|
2
3
|
export declare class ConflictError extends Error {
|
|
3
4
|
constructor();
|
|
@@ -9,22 +10,60 @@ export declare class StarfishHttpError extends Error {
|
|
|
9
10
|
constructor(status: number, body: string);
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* v3.0 cap-cert provider for `StarfishClient`. Returns the device's cap-cert and
|
|
14
|
+
* the matching Ed25519 private key (hex). The client calls `getCap()` once per
|
|
15
|
+
* outgoing request; implementations are expected to cache so this is cheap.
|
|
16
|
+
*
|
|
17
|
+
* When set, the client signs every outgoing request: each call carries
|
|
18
|
+
* `Authorization: Cap <base64(stableStringify(cap))>` plus `X-Starfish-Sig`,
|
|
19
|
+
* `X-Starfish-Ts`, `X-Starfish-Nonce`.
|
|
14
20
|
*/
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
export interface StarfishCapProvider {
|
|
22
|
+
/**
|
|
23
|
+
* Returns the device's cap-cert and its Ed25519 private key (hex).
|
|
24
|
+
* Implementations are expected to cache; the client may call this once per
|
|
25
|
+
* authenticated request.
|
|
26
|
+
*/
|
|
27
|
+
getCap(): Promise<{
|
|
28
|
+
cap: CapCert;
|
|
29
|
+
devEdPrivHex: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
20
32
|
/** Options for creating a StarfishClient. */
|
|
21
33
|
export interface StarfishClientOptions {
|
|
22
34
|
/** Base URL of the Starfish server (e.g. "https://api.example.com/v1"). */
|
|
23
35
|
baseUrl: string;
|
|
24
|
-
/**
|
|
25
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Cap-cert provider. When set, requests are signed with Ed25519 and carry
|
|
38
|
+
* `Authorization: Cap <…>`. Omit for unauthenticated public-read collections.
|
|
39
|
+
*/
|
|
40
|
+
capProvider?: StarfishCapProvider;
|
|
26
41
|
/** Optional fetch implementation (defaults to global fetch). */
|
|
27
42
|
fetch?: typeof fetch;
|
|
43
|
+
/**
|
|
44
|
+
* Optional list of client-side plugins. The list is stored on the client
|
|
45
|
+
* instance but does not fire any hooks yet — the contract is plumbed so
|
|
46
|
+
* extension packages (`starfish-identities`, `starfish-keyring`,
|
|
47
|
+
* `starfish-sharing`, …) can register against it later without a breaking
|
|
48
|
+
* API change.
|
|
49
|
+
*
|
|
50
|
+
* The current set of hooks is purposely empty; extensions that need to
|
|
51
|
+
* react to mint events or transport actions today can wrap the client
|
|
52
|
+
* directly. Future hook additions will be additive.
|
|
53
|
+
*/
|
|
54
|
+
plugins?: ClientPlugin[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Client-side plugin contract.
|
|
58
|
+
*
|
|
59
|
+
* A placeholder shape: the interface intentionally has no required hooks
|
|
60
|
+
* yet; extensions declare a plugin object with `name` and opt into
|
|
61
|
+
* specific lifecycle hooks once those exist. Apps wire plugins via
|
|
62
|
+
* `new StarfishClient({ plugins: [...] })`.
|
|
63
|
+
*/
|
|
64
|
+
export interface ClientPlugin {
|
|
65
|
+
/** Human-readable name. Used in error messages and audit output. */
|
|
66
|
+
name: string;
|
|
28
67
|
}
|
|
29
68
|
/** Conflict resolver: given local and remote data, return merged result. */
|
|
30
69
|
export type ConflictResolver = (local: Record<string, unknown>, remote: Record<string, unknown>) => Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakkar.software/starfish-client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-alpha.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/Drakkar-Software/starfish.git",
|
|
@@ -37,14 +37,6 @@
|
|
|
37
37
|
"./testing": {
|
|
38
38
|
"types": "./dist/testing.d.ts",
|
|
39
39
|
"import": "./dist/testing.js"
|
|
40
|
-
},
|
|
41
|
-
"./identity": {
|
|
42
|
-
"types": "./dist/identity.d.ts",
|
|
43
|
-
"import": "./dist/identity.js"
|
|
44
|
-
},
|
|
45
|
-
"./group": {
|
|
46
|
-
"types": "./dist/group-crypto.d.ts",
|
|
47
|
-
"import": "./dist/group-crypto.js"
|
|
48
40
|
}
|
|
49
41
|
},
|
|
50
42
|
"peerDependencies": {
|
|
@@ -68,20 +60,19 @@
|
|
|
68
60
|
}
|
|
69
61
|
},
|
|
70
62
|
"dependencies": {
|
|
71
|
-
"@
|
|
72
|
-
"@drakkar.software/starfish-protocol": "2.3.0"
|
|
63
|
+
"@drakkar.software/starfish-protocol": "3.0.0-alpha.0"
|
|
73
64
|
},
|
|
74
65
|
"devDependencies": {
|
|
75
66
|
"@legendapp/state": "^2.0.0",
|
|
76
67
|
"@testing-library/react": "^16.3.2",
|
|
77
68
|
"@types/react": "^19.2.14",
|
|
78
69
|
"@types/react-dom": "^19.2.3",
|
|
70
|
+
"esbuild": "^0.27.4",
|
|
79
71
|
"hono": "^4.12.7",
|
|
80
72
|
"immer": "^11.1.4",
|
|
81
73
|
"jsdom": "^29.0.1",
|
|
82
74
|
"react": "^19.2.4",
|
|
83
75
|
"react-dom": "^19.2.4",
|
|
84
|
-
"esbuild": "^0.27.4",
|
|
85
76
|
"typescript": "^5.5.0",
|
|
86
77
|
"vitest": "^3.0.0",
|
|
87
78
|
"zustand": "^5.0.11"
|
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,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
|
-
}
|