@drakkar.software/starfish-client 3.0.0-alpha.16 → 3.0.0-alpha.19
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/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/zustand.js +10 -0
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +7 -0
- package/dist/client.js +37 -316
- package/dist/crypto.js +49 -0
- package/dist/entitlements.js +41 -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/identity.d.ts +82 -4
- package/dist/identity.js +354 -2
- package/dist/identity.js.map +4 -4
- package/dist/index.js +11 -1
- package/dist/index.js.map +2 -2
- package/dist/mobile-lifecycle.js +2 -41
- package/dist/polling.js +2 -2
- package/dist/sync.js +14 -68
- package/package.json +2 -2
- package/dist/_crypto_helpers.d.ts +0 -4
- package/dist/append-log.js +0 -267
- 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/mobile-lifecycle.js
CHANGED
|
@@ -31,13 +31,13 @@ export function createMobileLifecycle(store, deps, options = {}) {
|
|
|
31
31
|
const appSub = deps.appState.addEventListener("change", (appState) => {
|
|
32
32
|
if (appState === "background" && flushOnBackground) {
|
|
33
33
|
if (store.getState().dirty) {
|
|
34
|
-
store.getState().flush().catch((
|
|
34
|
+
store.getState().flush().catch(() => { });
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
else if (appState === "active" && pullOnForeground) {
|
|
38
38
|
const { online, syncing } = store.getState();
|
|
39
39
|
if (online && !syncing) {
|
|
40
|
-
store.getState().pull().catch((
|
|
40
|
+
store.getState().pull().catch(() => { });
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
// "inactive" (iOS transition) and other states are intentionally ignored
|
|
@@ -53,42 +53,3 @@ export function createMobileLifecycle(store, deps, options = {}) {
|
|
|
53
53
|
netUnsub?.();
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Wires React Native app lifecycle events to an append-log store
|
|
58
|
-
* (`createStarfishLog`). A log is read-only, so this only pulls on foreground
|
|
59
|
-
* (there is nothing to flush on background). NetInfo connectivity changes are
|
|
60
|
-
* forwarded to `store.getState().setOnline()`.
|
|
61
|
-
*
|
|
62
|
-
* ```ts
|
|
63
|
-
* import { AppState } from "react-native"
|
|
64
|
-
* import NetInfo from "@react-native-community/netinfo"
|
|
65
|
-
* import { createStarfishLog, createAppendLogMobileLifecycle } from "@drakkar.software/starfish-client"
|
|
66
|
-
*
|
|
67
|
-
* const store = createStarfishLog({ cursor })
|
|
68
|
-
* const cleanup = createAppendLogMobileLifecycle(store, { appState: AppState, netInfo: NetInfo })
|
|
69
|
-
* useEffect(() => cleanup, [])
|
|
70
|
-
* ```
|
|
71
|
-
*
|
|
72
|
-
* @returns A cleanup function that removes all event listeners.
|
|
73
|
-
*/
|
|
74
|
-
export function createAppendLogMobileLifecycle(store, deps, options = {}) {
|
|
75
|
-
const { pullOnForeground = true } = options;
|
|
76
|
-
const appSub = deps.appState.addEventListener("change", (appState) => {
|
|
77
|
-
if (appState === "active" && pullOnForeground) {
|
|
78
|
-
const { online, loading } = store.getState();
|
|
79
|
-
if (online && !loading) {
|
|
80
|
-
store.getState().pull().catch((err) => { console.error("[Starfish] foreground log pull failed:", err); });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
let netUnsub = null;
|
|
85
|
-
if (deps.netInfo) {
|
|
86
|
-
netUnsub = deps.netInfo.addEventListener(({ isConnected }) => {
|
|
87
|
-
store.getState().setOnline(!!isConnected);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
return () => {
|
|
91
|
-
appSub.remove();
|
|
92
|
-
netUnsub?.();
|
|
93
|
-
};
|
|
94
|
-
}
|
package/dist/polling.js
CHANGED
|
@@ -14,7 +14,7 @@ export function startPolling(pullFn, getState, intervalMs = 30_000) {
|
|
|
14
14
|
const timer = setInterval(() => {
|
|
15
15
|
const { online, syncing } = getState();
|
|
16
16
|
if (online && !syncing)
|
|
17
|
-
pullFn().catch((
|
|
17
|
+
pullFn().catch(() => { });
|
|
18
18
|
}, intervalMs);
|
|
19
19
|
return () => clearInterval(timer);
|
|
20
20
|
}
|
|
@@ -42,7 +42,7 @@ export function startAdaptivePolling(pullFn, getState, options) {
|
|
|
42
42
|
return;
|
|
43
43
|
const { online, syncing } = getState();
|
|
44
44
|
if (online && !syncing)
|
|
45
|
-
pullFn().catch((
|
|
45
|
+
pullFn().catch(() => { });
|
|
46
46
|
}, intervalMs);
|
|
47
47
|
return {
|
|
48
48
|
pause: () => { paused = true; },
|
package/dist/sync.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { deepMerge, stableStringify } from "@drakkar.software/starfish-protocol";
|
|
2
2
|
import { ConflictError } from "./types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createEncryptor } from "./crypto.js";
|
|
4
4
|
import { ValidationError } from "./validate.js";
|
|
5
|
-
export class AbortError extends Error {
|
|
6
|
-
constructor() {
|
|
7
|
-
super("SyncManager was aborted");
|
|
8
|
-
this.name = "AbortError";
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
5
|
export class SyncManager {
|
|
12
6
|
client;
|
|
13
7
|
pullPath;
|
|
@@ -15,31 +9,28 @@ export class SyncManager {
|
|
|
15
9
|
onConflict;
|
|
16
10
|
maxRetries;
|
|
17
11
|
encryptor;
|
|
18
|
-
|
|
12
|
+
signData;
|
|
19
13
|
logger;
|
|
20
14
|
loggerName;
|
|
21
15
|
validate;
|
|
22
16
|
lastHash = null;
|
|
23
17
|
lastCheckpoint = 0;
|
|
24
18
|
localData = {};
|
|
25
|
-
aborted = false;
|
|
26
19
|
constructor(options) {
|
|
27
20
|
this.client = options.client;
|
|
28
21
|
this.pullPath = options.pullPath;
|
|
29
22
|
this.pushPath = options.pushPath;
|
|
30
23
|
this.onConflict = options.onConflict ?? deepMerge;
|
|
31
24
|
this.maxRetries = options.maxRetries ?? 3;
|
|
32
|
-
this.
|
|
25
|
+
this.signData = options.signData;
|
|
33
26
|
this.logger = options.logger;
|
|
34
27
|
this.loggerName = options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
|
|
35
28
|
this.validate = options.validate;
|
|
36
|
-
this.encryptor =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
get isAborted() {
|
|
42
|
-
return this.aborted;
|
|
29
|
+
this.encryptor =
|
|
30
|
+
options.encryptor ??
|
|
31
|
+
(options.encryptionSecret && options.encryptionSalt
|
|
32
|
+
? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)
|
|
33
|
+
: null);
|
|
43
34
|
}
|
|
44
35
|
getData() {
|
|
45
36
|
return { ...this.localData };
|
|
@@ -47,31 +38,16 @@ export class SyncManager {
|
|
|
47
38
|
getHash() {
|
|
48
39
|
return this.lastHash;
|
|
49
40
|
}
|
|
50
|
-
/** Set the last-known server hash. Used by persistence layers to restore state across restarts. */
|
|
51
|
-
setHash(hash) {
|
|
52
|
-
this.lastHash = hash;
|
|
53
|
-
}
|
|
54
41
|
getCheckpoint() {
|
|
55
42
|
return this.lastCheckpoint;
|
|
56
43
|
}
|
|
57
44
|
async pull() {
|
|
58
|
-
if (this.aborted)
|
|
59
|
-
throw new AbortError();
|
|
60
45
|
this.logger?.pullStart(this.loggerName);
|
|
61
46
|
const start = performance.now();
|
|
62
47
|
try {
|
|
63
|
-
// NOTE: `SyncManager.pull` does NOT auto-enable `withKeyring`. Clients
|
|
64
|
-
// that drive the keyring helpers from `recipients.ts` and want to save
|
|
65
|
-
// the cold-start round-trip should call `client.pull(path, {withKeyring: true})`
|
|
66
|
-
// directly. We keep `SyncManager` keyring-agnostic so it stays usable
|
|
67
|
-
// for collections that don't use delegated encryption.
|
|
68
48
|
const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
|
|
69
|
-
if (this.aborted)
|
|
70
|
-
throw new AbortError();
|
|
71
49
|
if (this.encryptor) {
|
|
72
50
|
const decrypted = await this.encryptor.decrypt(result.data);
|
|
73
|
-
if (this.aborted)
|
|
74
|
-
throw new AbortError();
|
|
75
51
|
this.localData = decrypted;
|
|
76
52
|
result.data = decrypted;
|
|
77
53
|
}
|
|
@@ -93,8 +69,6 @@ export class SyncManager {
|
|
|
93
69
|
}
|
|
94
70
|
}
|
|
95
71
|
async push(data) {
|
|
96
|
-
if (this.aborted)
|
|
97
|
-
throw new AbortError();
|
|
98
72
|
if (this.validate) {
|
|
99
73
|
const result = this.validate(data);
|
|
100
74
|
if (result !== true)
|
|
@@ -106,33 +80,13 @@ export class SyncManager {
|
|
|
106
80
|
let pendingData = data;
|
|
107
81
|
while (attempt <= this.maxRetries) {
|
|
108
82
|
try {
|
|
109
|
-
const
|
|
83
|
+
const payload = this.encryptor
|
|
110
84
|
? await this.encryptor.encrypt(pendingData)
|
|
111
85
|
: pendingData;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// top-level body siblings of `data` (NOT inside `data`), where the server
|
|
117
|
-
// verifies it and stores the raw author pubkey.
|
|
118
|
-
let author;
|
|
119
|
-
if (this.signer) {
|
|
120
|
-
const { devEdPubHex, sign } = await this.signer.getSigner();
|
|
121
|
-
if (this.aborted)
|
|
122
|
-
throw new AbortError();
|
|
123
|
-
const documentKey = stripPushPrefix(this.pushPath);
|
|
124
|
-
const canonical = docAuthorCanonicalInput(documentKey, sealed);
|
|
125
|
-
const sigBytes = await sign(new TextEncoder().encode(canonical));
|
|
126
|
-
if (this.aborted)
|
|
127
|
-
throw new AbortError();
|
|
128
|
-
author = {
|
|
129
|
-
[AUTHOR_PUBKEY_FIELD]: devEdPubHex,
|
|
130
|
-
[AUTHOR_SIGNATURE_FIELD]: getBase64().encode(sigBytes),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
const result = await this.client.push(this.pushPath, sealed, this.lastHash, author);
|
|
134
|
-
if (this.aborted)
|
|
135
|
-
throw new AbortError();
|
|
86
|
+
const sig = this.signData
|
|
87
|
+
? await this.signData(stableStringify(payload))
|
|
88
|
+
: undefined;
|
|
89
|
+
const result = await this.client.push(this.pushPath, payload, this.lastHash, sig);
|
|
136
90
|
this.lastHash = result.hash;
|
|
137
91
|
this.lastCheckpoint = result.timestamp;
|
|
138
92
|
this.localData = pendingData;
|
|
@@ -140,8 +94,6 @@ export class SyncManager {
|
|
|
140
94
|
return result;
|
|
141
95
|
}
|
|
142
96
|
catch (err) {
|
|
143
|
-
if (err instanceof AbortError)
|
|
144
|
-
throw err;
|
|
145
97
|
if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {
|
|
146
98
|
this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err));
|
|
147
99
|
throw err;
|
|
@@ -149,20 +101,14 @@ export class SyncManager {
|
|
|
149
101
|
this.logger?.conflict(this.loggerName, attempt + 1);
|
|
150
102
|
try {
|
|
151
103
|
const remote = await this.client.pull(this.pullPath);
|
|
152
|
-
if (this.aborted)
|
|
153
|
-
throw new AbortError();
|
|
154
104
|
const remoteData = this.encryptor
|
|
155
105
|
? await this.encryptor.decrypt(remote.data)
|
|
156
106
|
: remote.data;
|
|
157
|
-
if (this.aborted)
|
|
158
|
-
throw new AbortError();
|
|
159
107
|
this.lastHash = remote.hash;
|
|
160
108
|
this.lastCheckpoint = remote.timestamp;
|
|
161
109
|
pendingData = this.onConflict(pendingData, remoteData);
|
|
162
110
|
}
|
|
163
111
|
catch (resolveErr) {
|
|
164
|
-
if (resolveErr instanceof AbortError)
|
|
165
|
-
throw resolveErr;
|
|
166
112
|
const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr);
|
|
167
113
|
this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`);
|
|
168
114
|
throw resolveErr;
|
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.19",
|
|
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.19"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@legendapp/state": "^2.0.0",
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare function hkdfBytes(ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, lengthBytes?: number): Promise<Uint8Array>;
|
|
2
|
-
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
3
|
-
export declare function hexToBytes(hex: string): Uint8Array;
|
|
4
|
-
export declare function concat(...parts: Uint8Array[]): Uint8Array;
|
package/dist/append-log.js
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { verifyAppendAuthor, } from "@drakkar.software/starfish-protocol";
|
|
2
|
-
/** The `/pull/` action prefix; mirrors `PUSH_PATH_PREFIX` for the read side. */
|
|
3
|
-
const PULL_PATH_PREFIX = "/pull/";
|
|
4
|
-
/** The storage `documentKey` for a pull `path`: the path with the `/pull/`
|
|
5
|
-
* action prefix stripped (the namespace lives only in the URL). The author
|
|
6
|
-
* signature binds to this key, so a reader re-derives it the same way the
|
|
7
|
-
* writer did from `/push/…`. */
|
|
8
|
-
function stripPullPrefix(path) {
|
|
9
|
-
return path.startsWith(PULL_PATH_PREFIX) ? path.slice(PULL_PATH_PREFIX.length) : path;
|
|
10
|
-
}
|
|
11
|
-
/** Thrown when an append element's author signature fails verification. */
|
|
12
|
-
export class AppendAuthorError extends Error {
|
|
13
|
-
ts;
|
|
14
|
-
constructor(ts) {
|
|
15
|
-
super(`append element author verification failed (ts=${ts})`);
|
|
16
|
-
this.ts = ts;
|
|
17
|
-
this.name = "AppendAuthorError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/** Largest `ts` among `items`, or `0` when empty. The checkpoint for an
|
|
21
|
-
* append-only log is exactly this — the server returns elements with
|
|
22
|
-
* `ts > checkpoint`, and element timestamps are strictly increasing. */
|
|
23
|
-
export function checkpointOf(items) {
|
|
24
|
-
let max = 0;
|
|
25
|
-
for (const it of items)
|
|
26
|
-
if (it.ts > max)
|
|
27
|
-
max = it.ts;
|
|
28
|
-
return max;
|
|
29
|
-
}
|
|
30
|
-
/** Copy the optional author fields from `src` onto a fresh element with `data`. */
|
|
31
|
-
function withAuthor(ts, data, src) {
|
|
32
|
-
const out = { ts, data };
|
|
33
|
-
if (src.authorPubkey !== undefined)
|
|
34
|
-
out.authorPubkey = src.authorPubkey;
|
|
35
|
-
if (src.authorSignature !== undefined)
|
|
36
|
-
out.authorSignature = src.authorSignature;
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* A stateful cursor over an append-only collection. It owns the accumulated
|
|
41
|
-
* array of elements and pulls only what is new: each {@link pull} derives the
|
|
42
|
-
* checkpoint from the last element it holds and asks the server for elements
|
|
43
|
-
* with a greater `ts`.
|
|
44
|
-
*
|
|
45
|
-
* This is the incremental, stateful counterpart to the deliberately stateless
|
|
46
|
-
* `client.pull(path, { appendField, since })`, and the sibling of
|
|
47
|
-
* {@link SyncManager} for append-only logs (no merge / push-conflict
|
|
48
|
-
* machinery — a log only grows).
|
|
49
|
-
*
|
|
50
|
-
* The cursor accumulates every pulled element in memory; for an unboundedly
|
|
51
|
-
* large log, pull a bounded window with raw `client.pull(path, { last })` instead.
|
|
52
|
-
*
|
|
53
|
-
* Cold start (nothing persisted) — first `pull()` fetches the whole collection:
|
|
54
|
-
* ```ts
|
|
55
|
-
* const log = new AppendLogCursor({ client, pullPath: "/pull/events" })
|
|
56
|
-
* const all = await log.pull()
|
|
57
|
-
* ```
|
|
58
|
-
* Warm start (resume from persisted data) — first `pull()` fetches only newer
|
|
59
|
-
* elements; persistence is a round-trip of `getItems()`:
|
|
60
|
-
* ```ts
|
|
61
|
-
* const log = new AppendLogCursor({ client, pullPath: "/pull/events",
|
|
62
|
-
* initialItems: await store.load() })
|
|
63
|
-
* const fresh = await log.pull()
|
|
64
|
-
* await store.save(log.getItems())
|
|
65
|
-
* ```
|
|
66
|
-
* Warm start for an **E2EE** log — persist ciphertext, render decrypted:
|
|
67
|
-
* ```ts
|
|
68
|
-
* const log = new AppendLogCursor({ client, pullPath: "/pull/streamchat",
|
|
69
|
-
* encryptor, persistEncrypted: true, onElementError: "skip",
|
|
70
|
-
* initialItems: await store.load() }) // ciphertext from disk
|
|
71
|
-
* const history = await log.getDecryptedItems() // render persisted history
|
|
72
|
-
* const fresh = await log.pull() // decrypted delta
|
|
73
|
-
* await store.save(log.getItems()) // ciphertext back to disk
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
export class AppendLogCursor {
|
|
77
|
-
client;
|
|
78
|
-
pullPath;
|
|
79
|
-
appendField;
|
|
80
|
-
encryptor;
|
|
81
|
-
verifyAuthor;
|
|
82
|
-
onElementError;
|
|
83
|
-
persistEncrypted;
|
|
84
|
-
documentKey;
|
|
85
|
-
logger;
|
|
86
|
-
loggerName;
|
|
87
|
-
items;
|
|
88
|
-
lastCheckpoint;
|
|
89
|
-
/** Tail of the serialized pull chain. Concurrent `pull()` calls queue behind
|
|
90
|
-
* it so each runs against the checkpoint the previous one advanced — no two
|
|
91
|
-
* overlapping fetches read the same checkpoint and double-append a window. */
|
|
92
|
-
pullChain = Promise.resolve();
|
|
93
|
-
constructor(options) {
|
|
94
|
-
this.client = options.client;
|
|
95
|
-
this.pullPath = options.pullPath;
|
|
96
|
-
this.appendField = options.appendField ?? "items";
|
|
97
|
-
this.encryptor = options.encryptor;
|
|
98
|
-
this.verifyAuthor = options.verifyAuthor;
|
|
99
|
-
this.onElementError = options.onElementError ?? "throw";
|
|
100
|
-
this.persistEncrypted = options.persistEncrypted ?? false;
|
|
101
|
-
this.documentKey = stripPullPrefix(options.pullPath);
|
|
102
|
-
this.logger = options.logger;
|
|
103
|
-
this.loggerName =
|
|
104
|
-
options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
|
|
105
|
-
const seed = options.initialItems ?? [];
|
|
106
|
-
const seedCheckpoint = checkpointOf(seed);
|
|
107
|
-
if (options.since != null) {
|
|
108
|
-
if (options.since < 0)
|
|
109
|
-
throw new Error("since must be non-negative");
|
|
110
|
-
if (options.since < seedCheckpoint) {
|
|
111
|
-
throw new Error("since must be >= the max ts of initialItems");
|
|
112
|
-
}
|
|
113
|
-
this.lastCheckpoint = options.since;
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
this.lastCheckpoint = seedCheckpoint;
|
|
117
|
-
}
|
|
118
|
-
this.items = [...seed];
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Fetch elements newer than the current checkpoint, verify + decrypt them,
|
|
122
|
-
* append them to the local log, and return ONLY the newly-fetched batch
|
|
123
|
-
* (decrypted when an `encryptor` is set).
|
|
124
|
-
*
|
|
125
|
-
* Atomic under `onElementError: "throw"` (the default): the batch is fully
|
|
126
|
-
* verified and decrypted into a local before any state mutation, so a
|
|
127
|
-
* verify/decrypt failure throws without advancing the checkpoint past elements
|
|
128
|
-
* that could never be re-fetched. Under `"skip"`, a failing element is dropped
|
|
129
|
-
* from the returned batch but the checkpoint still advances past it.
|
|
130
|
-
*
|
|
131
|
-
* Safe to call concurrently: overlapping calls are serialized internally, so
|
|
132
|
-
* each runs against the checkpoint the previous one advanced (no double-fetch
|
|
133
|
-
* of the same window). The next pull after one completes will pick up anything
|
|
134
|
-
* that arrived in between.
|
|
135
|
-
*/
|
|
136
|
-
async pull() {
|
|
137
|
-
// Chain onto the previous pull (whether it resolved or rejected) so calls
|
|
138
|
-
// run one-at-a-time against the latest checkpoint. `pullChain` swallows
|
|
139
|
-
// outcomes to stay alive; the caller still sees this call's real result.
|
|
140
|
-
const run = this.pullChain.then(() => this.doPull(), () => this.doPull());
|
|
141
|
-
this.pullChain = run.then(() => undefined, () => undefined);
|
|
142
|
-
return run;
|
|
143
|
-
}
|
|
144
|
-
async doPull() {
|
|
145
|
-
this.logger?.pullStart(this.loggerName);
|
|
146
|
-
const start = performance.now();
|
|
147
|
-
try {
|
|
148
|
-
const since = this.lastCheckpoint;
|
|
149
|
-
// Omit `since` on cold start so the request carries no `?checkpoint=`.
|
|
150
|
-
const opts = since > 0 ? { appendField: this.appendField, since } : { appendField: this.appendField };
|
|
151
|
-
const raw = await this.client.pull(this.pullPath, opts);
|
|
152
|
-
const batch = []; // decrypted, returned to the caller
|
|
153
|
-
const stored = []; // what we keep in `items` (cipher- or plaintext)
|
|
154
|
-
let maxTs = since;
|
|
155
|
-
let skipped = 0;
|
|
156
|
-
for (const el of raw) {
|
|
157
|
-
// Defensive: guard a misbehaving/mocked server from making us
|
|
158
|
-
// double-append a held element. Gated on `since > 0` to mirror the
|
|
159
|
-
// server (which only filters when checkpoint > 0): on a cold start
|
|
160
|
-
// `since` is 0 and we must NOT drop a legitimate `ts: 0` first element.
|
|
161
|
-
if (since > 0 && el.ts <= since)
|
|
162
|
-
continue;
|
|
163
|
-
// Advance past every windowed element BEFORE verify/decrypt so a skipped
|
|
164
|
-
// element still moves the checkpoint and is never re-fetched.
|
|
165
|
-
if (el.ts > maxTs)
|
|
166
|
-
maxTs = el.ts;
|
|
167
|
-
let decrypted = null;
|
|
168
|
-
try {
|
|
169
|
-
this.verifyOne(el);
|
|
170
|
-
const data = this.encryptor ? await this.encryptor.decrypt(el.data) : el.data;
|
|
171
|
-
decrypted = withAuthor(el.ts, data, el);
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
// "throw" rethrows here, before any state mutation below — atomic.
|
|
175
|
-
if (this.onElementError !== "skip")
|
|
176
|
-
throw err;
|
|
177
|
-
skipped++;
|
|
178
|
-
}
|
|
179
|
-
if (this.persistEncrypted) {
|
|
180
|
-
// Keep the original ciphertext envelope (even for a skipped element:
|
|
181
|
-
// it is valid data we simply cannot read now — a later key might).
|
|
182
|
-
stored.push(withAuthor(el.ts, el.data, el));
|
|
183
|
-
}
|
|
184
|
-
else if (decrypted) {
|
|
185
|
-
stored.push(decrypted);
|
|
186
|
-
}
|
|
187
|
-
if (decrypted)
|
|
188
|
-
batch.push(decrypted);
|
|
189
|
-
}
|
|
190
|
-
this.items.push(...stored);
|
|
191
|
-
this.lastCheckpoint = maxTs;
|
|
192
|
-
this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start), skipped > 0 ? { skippedCount: skipped } : undefined);
|
|
193
|
-
return batch;
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err));
|
|
197
|
-
throw err;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
/** Verify a single element's author signature over its RAW (pre-decryption)
|
|
201
|
-
* `data`. Throws {@link AppendAuthorError} on any failure. No-op when
|
|
202
|
-
* verification is disabled. */
|
|
203
|
-
verifyOne(el) {
|
|
204
|
-
if (!this.verifyAuthor)
|
|
205
|
-
return;
|
|
206
|
-
const policy = typeof this.verifyAuthor === "object" ? this.verifyAuthor : {};
|
|
207
|
-
const { authorPubkey, authorSignature } = el;
|
|
208
|
-
if (!authorPubkey || !authorSignature)
|
|
209
|
-
throw new AppendAuthorError(el.ts);
|
|
210
|
-
// Public keys are hex, which is case-insensitive — compare normalized so a
|
|
211
|
-
// caller passing a differently-cased `expectedAuthorPubkey` isn't falsely rejected.
|
|
212
|
-
if (policy.expectedAuthorPubkey &&
|
|
213
|
-
authorPubkey.toLowerCase() !== policy.expectedAuthorPubkey.toLowerCase()) {
|
|
214
|
-
throw new AppendAuthorError(el.ts);
|
|
215
|
-
}
|
|
216
|
-
void policy;
|
|
217
|
-
const ok = verifyAppendAuthor(this.documentKey, el.data, authorPubkey, authorSignature);
|
|
218
|
-
if (!ok)
|
|
219
|
-
throw new AppendAuthorError(el.ts);
|
|
220
|
-
}
|
|
221
|
-
/** The full accumulated log (a shallow copy), in `ts` order. Under
|
|
222
|
-
* `persistEncrypted` these carry CIPHERTEXT `data` (persist them as-is, then
|
|
223
|
-
* re-seed via `initialItems`); otherwise they carry decrypted/plaintext data. */
|
|
224
|
-
getItems() {
|
|
225
|
-
return [...this.items];
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* The full accumulated log, DECRYPTED — for rendering warm-started history in
|
|
229
|
-
* `persistEncrypted` mode (where {@link getItems} holds ciphertext). Honors
|
|
230
|
-
* `onElementError` (a `"skip"` cursor drops elements it cannot read). When the
|
|
231
|
-
* cursor has no `encryptor`, or is not in `persistEncrypted` mode, the held
|
|
232
|
-
* elements are already plaintext/decrypted and are returned as-is.
|
|
233
|
-
*/
|
|
234
|
-
async getDecryptedItems() {
|
|
235
|
-
const snapshot = [...this.items];
|
|
236
|
-
if (!this.encryptor || !this.persistEncrypted)
|
|
237
|
-
return snapshot;
|
|
238
|
-
const out = [];
|
|
239
|
-
for (const el of snapshot) {
|
|
240
|
-
try {
|
|
241
|
-
this.verifyOne(el);
|
|
242
|
-
const data = await this.encryptor.decrypt(el.data);
|
|
243
|
-
out.push(withAuthor(el.ts, data, el));
|
|
244
|
-
}
|
|
245
|
-
catch (err) {
|
|
246
|
-
if (this.onElementError !== "skip")
|
|
247
|
-
throw err;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return out;
|
|
251
|
-
}
|
|
252
|
-
/** The current checkpoint: the max `ts` held (the next pull's `since`). `0`
|
|
253
|
-
* when nothing has been pulled or seeded. */
|
|
254
|
-
getCheckpoint() {
|
|
255
|
-
return this.lastCheckpoint;
|
|
256
|
-
}
|
|
257
|
-
/** Restore the checkpoint without seeding items — for persistence layers that
|
|
258
|
-
* store only the checkpoint. Used to resume incrementally across restarts.
|
|
259
|
-
* Rejects a value below the max `ts` already held: rewinding would make the
|
|
260
|
-
* next pull re-deliver, and duplicate, elements the cursor already has. */
|
|
261
|
-
setCheckpoint(ts) {
|
|
262
|
-
if (ts < checkpointOf(this.items)) {
|
|
263
|
-
throw new Error("checkpoint must be >= the max ts already held");
|
|
264
|
-
}
|
|
265
|
-
this.lastCheckpoint = ts;
|
|
266
|
-
}
|
|
267
|
-
}
|
package/dist/cap-mint.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase-2 transitional shim. `mintDeviceCap` + `scopes.rootAll` now live in
|
|
3
|
-
* `@drakkar.software/starfish-identities`; `mintMemberCap` +
|
|
4
|
-
* `scopes.readOnly`/`scopes.writer`/`scopes.admin` now live in
|
|
5
|
-
* `@drakkar.software/starfish-sharing`. The `scopes` re-export here merges
|
|
6
|
-
* both so existing imports of `import { scopes } from
|
|
7
|
-
* "../src/cap-mint.js"` keep working. Removed in Phase 3.
|
|
8
|
-
*/
|
|
9
|
-
import { type ScopePreset as IdentityScopePreset, type MintOpts as IdentityMintOpts } from "@drakkar.software/starfish-identities";
|
|
10
|
-
export { mintDeviceCap } from "@drakkar.software/starfish-identities";
|
|
11
|
-
export { mintMemberCap } from "@drakkar.software/starfish-sharing";
|
|
12
|
-
/** @deprecated Phase-2 transitional facade. Use the per-package `scopes` directly. */
|
|
13
|
-
export declare const scopes: {
|
|
14
|
-
readOnly: (c: string) => import("@drakkar.software/starfish-sharing").ScopePreset;
|
|
15
|
-
writer: (c: string) => import("@drakkar.software/starfish-sharing").ScopePreset;
|
|
16
|
-
admin: (c: string) => import("@drakkar.software/starfish-sharing").ScopePreset;
|
|
17
|
-
rootAll: () => IdentityScopePreset;
|
|
18
|
-
};
|
|
19
|
-
export type ScopePreset = IdentityScopePreset;
|
|
20
|
-
export type MintOpts = IdentityMintOpts;
|
package/dist/cap-mint.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// src/cap-mint.ts
|
|
2
|
-
import { scopes as identityScopes } from "@drakkar.software/starfish-identities";
|
|
3
|
-
import { scopes as sharingScopes } from "@drakkar.software/starfish-sharing";
|
|
4
|
-
import { mintDeviceCap } from "@drakkar.software/starfish-identities";
|
|
5
|
-
import { mintMemberCap } from "@drakkar.software/starfish-sharing";
|
|
6
|
-
var scopes = { ...identityScopes, ...sharingScopes };
|
|
7
|
-
export {
|
|
8
|
-
mintDeviceCap,
|
|
9
|
-
mintMemberCap,
|
|
10
|
-
scopes
|
|
11
|
-
};
|
|
12
|
-
//# sourceMappingURL=cap-mint.js.map
|
package/dist/cap-mint.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/cap-mint.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Phase-2 transitional shim. `mintDeviceCap` + `scopes.rootAll` now live in\n * `@drakkar.software/starfish-identities`; `mintMemberCap` +\n * `scopes.readOnly`/`scopes.writer`/`scopes.admin` now live in\n * `@drakkar.software/starfish-sharing`. The `scopes` re-export here merges\n * both so existing imports of `import { scopes } from\n * \"../src/cap-mint.js\"` keep working. Removed in Phase 3.\n */\nimport { scopes as identityScopes, type ScopePreset as IdentityScopePreset, type MintOpts as IdentityMintOpts } from \"@drakkar.software/starfish-identities\"\nimport { scopes as sharingScopes } from \"@drakkar.software/starfish-sharing\"\n\nexport { mintDeviceCap } from \"@drakkar.software/starfish-identities\"\nexport { mintMemberCap } from \"@drakkar.software/starfish-sharing\"\n\n/** @deprecated Phase-2 transitional facade. Use the per-package `scopes` directly. */\nexport const scopes = { ...identityScopes, ...sharingScopes }\n\nexport type ScopePreset = IdentityScopePreset\nexport type MintOpts = IdentityMintOpts\n"],
|
|
5
|
-
"mappings": ";AAQA,SAAS,UAAU,sBAAkG;AACrH,SAAS,UAAU,qBAAqB;AAExC,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAGvB,IAAM,SAAS,EAAE,GAAG,gBAAgB,GAAG,cAAc;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/directory.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase-2 transitional shim. Device-directory helpers now live in
|
|
3
|
-
* `@drakkar.software/starfish-identities`; member-directory helpers now live
|
|
4
|
-
* in `@drakkar.software/starfish-sharing`. Removed in Phase 3.
|
|
5
|
-
*/
|
|
6
|
-
export { addDeviceEntry, listDevices, removeDeviceEntry, devicesPathFor, } from "@drakkar.software/starfish-identities";
|
|
7
|
-
export type { DirectoryEntry, Directory, DeviceEntry, ListDirectoryOpts, } from "@drakkar.software/starfish-identities";
|
|
8
|
-
export { addMemberEntry, listMembers, removeMemberEntry, membersPathFor, } from "@drakkar.software/starfish-sharing";
|
|
9
|
-
export type { MemberEntry } from "@drakkar.software/starfish-sharing";
|
package/dist/directory.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// src/directory.ts
|
|
2
|
-
import {
|
|
3
|
-
addDeviceEntry,
|
|
4
|
-
listDevices,
|
|
5
|
-
removeDeviceEntry,
|
|
6
|
-
devicesPathFor
|
|
7
|
-
} from "@drakkar.software/starfish-identities";
|
|
8
|
-
import {
|
|
9
|
-
addMemberEntry,
|
|
10
|
-
listMembers,
|
|
11
|
-
removeMemberEntry,
|
|
12
|
-
membersPathFor
|
|
13
|
-
} from "@drakkar.software/starfish-sharing";
|
|
14
|
-
export {
|
|
15
|
-
addDeviceEntry,
|
|
16
|
-
addMemberEntry,
|
|
17
|
-
devicesPathFor,
|
|
18
|
-
listDevices,
|
|
19
|
-
listMembers,
|
|
20
|
-
membersPathFor,
|
|
21
|
-
removeDeviceEntry,
|
|
22
|
-
removeMemberEntry
|
|
23
|
-
};
|
|
24
|
-
//# sourceMappingURL=directory.js.map
|
package/dist/directory.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/directory.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Phase-2 transitional shim. Device-directory helpers now live in\n * `@drakkar.software/starfish-identities`; member-directory helpers now live\n * in `@drakkar.software/starfish-sharing`. Removed in Phase 3.\n */\nexport {\n addDeviceEntry,\n listDevices,\n removeDeviceEntry,\n devicesPathFor,\n} from \"@drakkar.software/starfish-identities\"\nexport type {\n DirectoryEntry,\n Directory,\n DeviceEntry,\n ListDirectoryOpts,\n} from \"@drakkar.software/starfish-identities\"\n\nexport {\n addMemberEntry,\n listMembers,\n removeMemberEntry,\n membersPathFor,\n} from \"@drakkar.software/starfish-sharing\"\nexport type { MemberEntry } from \"@drakkar.software/starfish-sharing\"\n"],
|
|
5
|
-
"mappings": ";AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/keyring.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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
|
package/dist/keyring.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
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
|
-
}
|