@drakkar.software/starfish-client 2.2.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
package/dist/index.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { configurePlatform } from "@drakkar.software/starfish-protocol";
|
|
3
|
-
import { stableStringify as
|
|
3
|
+
import { stableStringify as stableStringify3, computeHash } from "@drakkar.software/starfish-protocol";
|
|
4
|
+
import { buildRevocationList, revocationListCanonicalSigningInput } from "@drakkar.software/starfish-protocol";
|
|
5
|
+
|
|
6
|
+
// src/client.ts
|
|
7
|
+
import {
|
|
8
|
+
signRequest,
|
|
9
|
+
stableStringify
|
|
10
|
+
} from "@drakkar.software/starfish-protocol";
|
|
4
11
|
|
|
5
12
|
// src/types.ts
|
|
6
13
|
var ConflictError = class extends Error {
|
|
@@ -20,34 +27,109 @@ var StarfishHttpError = class extends Error {
|
|
|
20
27
|
|
|
21
28
|
// src/client.ts
|
|
22
29
|
var APPEND_DEFAULT_FIELD = "items";
|
|
30
|
+
function encodeCapAuth(cap) {
|
|
31
|
+
const json = stableStringify(cap);
|
|
32
|
+
if (typeof btoa === "function") {
|
|
33
|
+
return btoa(json);
|
|
34
|
+
}
|
|
35
|
+
const bufCtor = globalThis.Buffer;
|
|
36
|
+
if (bufCtor) return bufCtor.from(json, "utf-8").toString("base64");
|
|
37
|
+
throw new Error("No base64 encoder available");
|
|
38
|
+
}
|
|
23
39
|
var StarfishClient = class {
|
|
24
40
|
baseUrl;
|
|
25
|
-
|
|
41
|
+
capProvider;
|
|
26
42
|
fetch;
|
|
43
|
+
/**
|
|
44
|
+
* Installed client-side plugins. Currently stored as inert data; no
|
|
45
|
+
* hooks fire yet. Extensions can inspect this list if needed.
|
|
46
|
+
*/
|
|
47
|
+
plugins;
|
|
27
48
|
constructor(options) {
|
|
28
49
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
29
|
-
this.
|
|
50
|
+
this.capProvider = options.capProvider;
|
|
30
51
|
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
52
|
+
this.plugins = options.plugins ? [...options.plugins] : [];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the host portion of the URL the client will send to. The host
|
|
56
|
+
* is folded into the signed canonical input as the `h` field so the
|
|
57
|
+
* server can refuse a signature that was minted against a different
|
|
58
|
+
* Starfish host (replay-across-servers defence).
|
|
59
|
+
*
|
|
60
|
+
* When `baseUrl` is relative — e.g. the consumer passed a custom `fetch`
|
|
61
|
+
* that resolves relative URLs in its own context — there is no parseable
|
|
62
|
+
* host; we return `""` so signing still proceeds. The server-side
|
|
63
|
+
* verifier will also reconstruct host from its inbound URL, so the
|
|
64
|
+
* empty-host case still verifies symmetrically when both sides agree.
|
|
65
|
+
*/
|
|
66
|
+
signingHost() {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(this.baseUrl).host;
|
|
69
|
+
} catch {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build auth headers for a request. When a `capProvider` is set, signs the
|
|
75
|
+
* request with the device's Ed25519 private key and returns the v3 header
|
|
76
|
+
* set (`Authorization: Cap …`, `X-Starfish-Sig`, `X-Starfish-Ts`,
|
|
77
|
+
* `X-Starfish-Nonce`). Empty when no provider is configured (public reads).
|
|
78
|
+
*
|
|
79
|
+
* Body bytes signed MUST equal the bytes sent on the wire — callers pass
|
|
80
|
+
* the already-serialized body string here so signing and transmission agree.
|
|
81
|
+
* The host bound into the signature is derived from `baseUrl` once per call.
|
|
82
|
+
*/
|
|
83
|
+
async buildAuthHeaders(method, pathAndQuery, body) {
|
|
84
|
+
if (this.capProvider) {
|
|
85
|
+
const { cap, devEdPrivHex } = await this.capProvider.getCap();
|
|
86
|
+
const req = {
|
|
87
|
+
method,
|
|
88
|
+
pathAndQuery,
|
|
89
|
+
body,
|
|
90
|
+
host: this.signingHost()
|
|
91
|
+
};
|
|
92
|
+
const { sig, ts, nonce } = await signRequest(req, devEdPrivHex);
|
|
93
|
+
return {
|
|
94
|
+
Authorization: `Cap ${encodeCapAuth(cap)}`,
|
|
95
|
+
"X-Starfish-Sig": sig,
|
|
96
|
+
"X-Starfish-Ts": String(ts),
|
|
97
|
+
"X-Starfish-Nonce": nonce
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
31
101
|
}
|
|
32
102
|
async pull(path, checkpointOrOptions) {
|
|
33
|
-
let
|
|
103
|
+
let pathAndQuery = path;
|
|
34
104
|
let appendField;
|
|
35
105
|
if (typeof checkpointOrOptions === "number") {
|
|
36
|
-
if (checkpointOrOptions)
|
|
106
|
+
if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`;
|
|
37
107
|
} else if (checkpointOrOptions != null) {
|
|
38
|
-
|
|
108
|
+
const opts = checkpointOrOptions;
|
|
109
|
+
const isPullOptions = opts.withKeyring !== void 0 || opts.checkpoint !== void 0;
|
|
39
110
|
const params = new URLSearchParams();
|
|
40
|
-
if (
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
111
|
+
if (isPullOptions) {
|
|
112
|
+
if (opts.checkpoint != null && opts.checkpoint > 0) {
|
|
113
|
+
params.set("checkpoint", String(opts.checkpoint));
|
|
114
|
+
}
|
|
115
|
+
if (opts.withKeyring) {
|
|
116
|
+
params.set("withKeyring", "1");
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
appendField = opts.appendField ?? APPEND_DEFAULT_FIELD;
|
|
120
|
+
if (opts.since != null) {
|
|
121
|
+
if (opts.since < 0) throw new Error("since must be non-negative");
|
|
122
|
+
params.set("checkpoint", String(opts.since));
|
|
123
|
+
}
|
|
124
|
+
if (opts.last != null) {
|
|
125
|
+
if (opts.last < 0) throw new Error("last must be non-negative");
|
|
126
|
+
params.set("last", String(opts.last));
|
|
127
|
+
}
|
|
47
128
|
}
|
|
48
|
-
if (params.size > 0)
|
|
129
|
+
if (params.size > 0) pathAndQuery += `?${params.toString()}`;
|
|
49
130
|
}
|
|
50
|
-
const
|
|
131
|
+
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
132
|
+
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
51
133
|
const res = await this.fetch(url, {
|
|
52
134
|
method: "GET",
|
|
53
135
|
headers: { Accept: "application/json", ...authHeaders }
|
|
@@ -67,16 +149,17 @@ var StarfishClient = class {
|
|
|
67
149
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
68
150
|
* @param data - The full document data to push
|
|
69
151
|
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
70
|
-
*
|
|
152
|
+
*
|
|
153
|
+
* v3 author fields (`authorPubkey` + `authorSignature`) live inside `data`
|
|
154
|
+
* and are produced by `SyncManager` when a `signer` is configured.
|
|
71
155
|
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
72
156
|
*/
|
|
73
|
-
async push(path, data, baseHash
|
|
157
|
+
async push(path, data, baseHash) {
|
|
74
158
|
const body = JSON.stringify({
|
|
75
159
|
data,
|
|
76
|
-
baseHash
|
|
77
|
-
...authorSignature && { authorSignature }
|
|
160
|
+
baseHash
|
|
78
161
|
});
|
|
79
|
-
const authHeaders =
|
|
162
|
+
const authHeaders = await this.buildAuthHeaders("POST", path, body);
|
|
80
163
|
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
81
164
|
method: "POST",
|
|
82
165
|
headers: {
|
|
@@ -99,7 +182,7 @@ var StarfishClient = class {
|
|
|
99
182
|
* Returns raw bytes with the content hash from the ETag header.
|
|
100
183
|
*/
|
|
101
184
|
async pullBlob(path) {
|
|
102
|
-
const authHeaders =
|
|
185
|
+
const authHeaders = await this.buildAuthHeaders("GET", path, void 0);
|
|
103
186
|
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
104
187
|
method: "GET",
|
|
105
188
|
headers: { Accept: "*/*", ...authHeaders }
|
|
@@ -117,7 +200,7 @@ var StarfishClient = class {
|
|
|
117
200
|
* Binary collections use last-write-wins (no conflict detection).
|
|
118
201
|
*/
|
|
119
202
|
async pushBlob(path, data, contentType) {
|
|
120
|
-
const authHeaders =
|
|
203
|
+
const authHeaders = await this.buildAuthHeaders("POST", path, void 0);
|
|
121
204
|
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
122
205
|
method: "POST",
|
|
123
206
|
headers: {
|
|
@@ -135,51 +218,7 @@ var StarfishClient = class {
|
|
|
135
218
|
};
|
|
136
219
|
|
|
137
220
|
// src/sync.ts
|
|
138
|
-
import { deepMerge, stableStringify } from "@drakkar.software/starfish-protocol";
|
|
139
|
-
|
|
140
|
-
// src/crypto.ts
|
|
141
|
-
import { getCrypto, getBase64, IV_BYTES, ENCRYPTED_KEY, deriveKey } from "@drakkar.software/starfish-protocol";
|
|
142
|
-
var ALGO = "AES-GCM";
|
|
143
|
-
function createEncryptor(secret, salt, info = "starfish-e2e") {
|
|
144
|
-
if (!secret) throw new Error("encryptionSecret must not be empty");
|
|
145
|
-
if (!salt) throw new Error("encryptionSalt must not be empty");
|
|
146
|
-
const keyPromise = deriveKey(secret, salt, info);
|
|
147
|
-
return {
|
|
148
|
-
async encrypt(data) {
|
|
149
|
-
const key = await keyPromise;
|
|
150
|
-
const c = getCrypto();
|
|
151
|
-
const b64 = getBase64();
|
|
152
|
-
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
153
|
-
const iv = c.getRandomValues(new Uint8Array(IV_BYTES));
|
|
154
|
-
const ciphertext = await c.subtle.encrypt({ name: ALGO, iv }, key, plaintext);
|
|
155
|
-
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
156
|
-
combined.set(iv);
|
|
157
|
-
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
158
|
-
return { [ENCRYPTED_KEY]: b64.encode(combined) };
|
|
159
|
-
},
|
|
160
|
-
async decrypt(wrapper) {
|
|
161
|
-
const encoded = wrapper[ENCRYPTED_KEY];
|
|
162
|
-
if (typeof encoded !== "string") {
|
|
163
|
-
throw new Error("Expected encrypted data but received unencrypted document");
|
|
164
|
-
}
|
|
165
|
-
const key = await keyPromise;
|
|
166
|
-
const c = getCrypto();
|
|
167
|
-
const b64 = getBase64();
|
|
168
|
-
const combined = b64.decode(encoded);
|
|
169
|
-
if (combined.length < IV_BYTES) {
|
|
170
|
-
throw new Error("Encrypted data is too short");
|
|
171
|
-
}
|
|
172
|
-
const iv = combined.slice(0, IV_BYTES);
|
|
173
|
-
const ciphertext = combined.slice(IV_BYTES);
|
|
174
|
-
try {
|
|
175
|
-
const plaintext = await c.subtle.decrypt({ name: ALGO, iv }, key, ciphertext);
|
|
176
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
177
|
-
} catch (err) {
|
|
178
|
-
throw new Error("Decryption failed: data may be tampered or key is incorrect", { cause: err });
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
}
|
|
221
|
+
import { deepMerge, getBase64, stableStringify as stableStringify2 } from "@drakkar.software/starfish-protocol";
|
|
183
222
|
|
|
184
223
|
// src/validate.ts
|
|
185
224
|
var ValidationError = class extends Error {
|
|
@@ -211,7 +250,7 @@ var SyncManager = class {
|
|
|
211
250
|
onConflict;
|
|
212
251
|
maxRetries;
|
|
213
252
|
encryptor;
|
|
214
|
-
|
|
253
|
+
signer;
|
|
215
254
|
logger;
|
|
216
255
|
loggerName;
|
|
217
256
|
validate;
|
|
@@ -225,11 +264,11 @@ var SyncManager = class {
|
|
|
225
264
|
this.pushPath = options.pushPath;
|
|
226
265
|
this.onConflict = options.onConflict ?? deepMerge;
|
|
227
266
|
this.maxRetries = options.maxRetries ?? 3;
|
|
228
|
-
this.
|
|
267
|
+
this.signer = options.signer;
|
|
229
268
|
this.logger = options.logger;
|
|
230
269
|
this.loggerName = options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
|
|
231
270
|
this.validate = options.validate;
|
|
232
|
-
this.encryptor = options.encryptor ??
|
|
271
|
+
this.encryptor = options.encryptor ?? null;
|
|
233
272
|
}
|
|
234
273
|
abort() {
|
|
235
274
|
this.aborted = true;
|
|
@@ -289,15 +328,25 @@ var SyncManager = class {
|
|
|
289
328
|
let pendingData = data;
|
|
290
329
|
while (attempt <= this.maxRetries) {
|
|
291
330
|
try {
|
|
292
|
-
const
|
|
293
|
-
if (this.aborted) throw new AbortError();
|
|
294
|
-
const sig = this.signData ? await this.signData(stableStringify(payload)) : void 0;
|
|
331
|
+
const sealed = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
295
332
|
if (this.aborted) throw new AbortError();
|
|
333
|
+
let payload = sealed;
|
|
334
|
+
if (this.signer) {
|
|
335
|
+
const { devEdPubHex, sign } = await this.signer.getSigner();
|
|
336
|
+
if (this.aborted) throw new AbortError();
|
|
337
|
+
const canonical = stableStringify2(sealed);
|
|
338
|
+
const sigBytes = await sign(new TextEncoder().encode(canonical));
|
|
339
|
+
if (this.aborted) throw new AbortError();
|
|
340
|
+
payload = {
|
|
341
|
+
...sealed,
|
|
342
|
+
authorPubkey: devEdPubHex,
|
|
343
|
+
authorSignature: getBase64().encode(sigBytes)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
296
346
|
const result = await this.client.push(
|
|
297
347
|
this.pushPath,
|
|
298
348
|
payload,
|
|
299
|
-
this.lastHash
|
|
300
|
-
sig
|
|
349
|
+
this.lastHash
|
|
301
350
|
);
|
|
302
351
|
if (this.aborted) throw new AbortError();
|
|
303
352
|
this.lastHash = result.hash;
|
|
@@ -339,6 +388,9 @@ var SyncManager = class {
|
|
|
339
388
|
}
|
|
340
389
|
};
|
|
341
390
|
|
|
391
|
+
// src/index.ts
|
|
392
|
+
import { ENCRYPTED_KEY } from "@drakkar.software/starfish-protocol";
|
|
393
|
+
|
|
342
394
|
// src/logger.ts
|
|
343
395
|
var consoleSyncLogger = {
|
|
344
396
|
pullStart: (s) => console.log(`[starfish:${s}] pull started`),
|
|
@@ -1171,170 +1223,6 @@ function createMultiStoreSync(options) {
|
|
|
1171
1223
|
}
|
|
1172
1224
|
return { serialize, restore, version };
|
|
1173
1225
|
}
|
|
1174
|
-
|
|
1175
|
-
// src/group-crypto.ts
|
|
1176
|
-
import { x25519 } from "@noble/curves/ed25519.js";
|
|
1177
|
-
import { getCrypto as getCrypto2, getBase64 as getBase642, IV_BYTES as IV_BYTES2, deriveKey as deriveKey2 } from "@drakkar.software/starfish-protocol";
|
|
1178
|
-
function bytesToHex(bytes) {
|
|
1179
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1180
|
-
}
|
|
1181
|
-
function hexToBytes(hex) {
|
|
1182
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
1183
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
1184
|
-
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1185
|
-
}
|
|
1186
|
-
return bytes;
|
|
1187
|
-
}
|
|
1188
|
-
var ALGO2 = "AES-GCM";
|
|
1189
|
-
var GROUP_WRAP_SALT = "starfish-group-wrap";
|
|
1190
|
-
var GROUP_WRAP_INFO = "starfish-group-wrap";
|
|
1191
|
-
var GROUP_ECDH_DOMAIN = "starfish-group-ecdh";
|
|
1192
|
-
var GROUP_DATA_INFO = "starfish-group";
|
|
1193
|
-
var GEK_BYTES = 32;
|
|
1194
|
-
async function deriveGroupKeyPair(passphrase, userId) {
|
|
1195
|
-
const c = getCrypto2();
|
|
1196
|
-
const enc = new TextEncoder();
|
|
1197
|
-
const input = enc.encode(`${passphrase}:${userId}:${GROUP_ECDH_DOMAIN}`);
|
|
1198
|
-
const hash = await c.subtle.digest("SHA-256", input);
|
|
1199
|
-
const privateKeyBytes = new Uint8Array(hash);
|
|
1200
|
-
const publicKeyBytes = x25519.getPublicKey(privateKeyBytes);
|
|
1201
|
-
return { privateKey: bytesToHex(privateKeyBytes), publicKey: bytesToHex(publicKeyBytes) };
|
|
1202
|
-
}
|
|
1203
|
-
function generateGroupKey() {
|
|
1204
|
-
const c = getCrypto2();
|
|
1205
|
-
return bytesToHex(c.getRandomValues(new Uint8Array(GEK_BYTES)));
|
|
1206
|
-
}
|
|
1207
|
-
async function wrapGroupKey(gek, memberPublicKey, wrapperPrivateKey) {
|
|
1208
|
-
const sharedSecret = x25519.getSharedSecret(hexToBytes(wrapperPrivateKey), hexToBytes(memberPublicKey));
|
|
1209
|
-
const wrappingKey = await deriveKey2(bytesToHex(sharedSecret), GROUP_WRAP_SALT, GROUP_WRAP_INFO);
|
|
1210
|
-
const c = getCrypto2();
|
|
1211
|
-
const b64 = getBase642();
|
|
1212
|
-
const iv = c.getRandomValues(new Uint8Array(IV_BYTES2));
|
|
1213
|
-
const encrypted = await c.subtle.encrypt({ name: ALGO2, iv }, wrappingKey, hexToBytes(gek).buffer);
|
|
1214
|
-
const combined = new Uint8Array(IV_BYTES2 + encrypted.byteLength);
|
|
1215
|
-
combined.set(iv);
|
|
1216
|
-
combined.set(new Uint8Array(encrypted), IV_BYTES2);
|
|
1217
|
-
return b64.encode(combined);
|
|
1218
|
-
}
|
|
1219
|
-
async function unwrapGroupKey(wrapped, memberPrivateKey, adminPublicKey) {
|
|
1220
|
-
const sharedSecret = x25519.getSharedSecret(hexToBytes(memberPrivateKey), hexToBytes(adminPublicKey));
|
|
1221
|
-
const wrappingKey = await deriveKey2(bytesToHex(sharedSecret), GROUP_WRAP_SALT, GROUP_WRAP_INFO);
|
|
1222
|
-
const b64 = getBase642();
|
|
1223
|
-
const c = getCrypto2();
|
|
1224
|
-
const combined = b64.decode(wrapped);
|
|
1225
|
-
const iv = combined.slice(0, IV_BYTES2);
|
|
1226
|
-
const ciphertext = combined.slice(IV_BYTES2);
|
|
1227
|
-
try {
|
|
1228
|
-
const decrypted = await c.subtle.decrypt({ name: ALGO2, iv }, wrappingKey, ciphertext);
|
|
1229
|
-
return bytesToHex(new Uint8Array(decrypted));
|
|
1230
|
-
} catch {
|
|
1231
|
-
throw new Error("Failed to unwrap group key: decryption failed (wrong keys or corrupted data)");
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
async function createGroupKeyring(adminKeyPair, members, gek) {
|
|
1235
|
-
const resolvedGek = gek ?? generateGroupKey();
|
|
1236
|
-
const wrappedKeys = {};
|
|
1237
|
-
for (const [memberId, memberPublicKey] of Object.entries(members)) {
|
|
1238
|
-
wrappedKeys[memberId] = await wrapGroupKey(resolvedGek, memberPublicKey, adminKeyPair.privateKey);
|
|
1239
|
-
}
|
|
1240
|
-
const keyring = {
|
|
1241
|
-
currentEpoch: 1,
|
|
1242
|
-
epochs: {
|
|
1243
|
-
"1": { adminPublicKey: adminKeyPair.publicKey, wrappedKeys }
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
return { keyring, gek: resolvedGek };
|
|
1247
|
-
}
|
|
1248
|
-
async function addGroupMember(keyring, adminKeyPair, currentGek, newMemberId, newMemberPublicKey) {
|
|
1249
|
-
const epochKey = String(keyring.currentEpoch);
|
|
1250
|
-
const epochKeyring = keyring.epochs[epochKey];
|
|
1251
|
-
if (!epochKeyring) throw new Error(`Epoch ${keyring.currentEpoch} not found in keyring`);
|
|
1252
|
-
if (epochKeyring.adminPublicKey !== adminKeyPair.publicKey) {
|
|
1253
|
-
throw new Error(`Provided key pair does not match the admin public key stored in epoch ${keyring.currentEpoch}`);
|
|
1254
|
-
}
|
|
1255
|
-
const wrapped = await wrapGroupKey(currentGek, newMemberPublicKey, adminKeyPair.privateKey);
|
|
1256
|
-
return {
|
|
1257
|
-
...keyring,
|
|
1258
|
-
epochs: {
|
|
1259
|
-
...keyring.epochs,
|
|
1260
|
-
[epochKey]: {
|
|
1261
|
-
...epochKeyring,
|
|
1262
|
-
wrappedKeys: { ...epochKeyring.wrappedKeys, [newMemberId]: wrapped }
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
};
|
|
1266
|
-
}
|
|
1267
|
-
async function rotateGroupKey(keyring, adminKeyPair, remainingMembers, newGek) {
|
|
1268
|
-
const epochKey = String(keyring.currentEpoch);
|
|
1269
|
-
const epochKeyring = keyring.epochs[epochKey];
|
|
1270
|
-
if (epochKeyring && epochKeyring.adminPublicKey !== adminKeyPair.publicKey) {
|
|
1271
|
-
throw new Error(
|
|
1272
|
-
`Provided key pair does not match the admin public key stored in epoch ${keyring.currentEpoch}`
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
const resolvedGek = newGek ?? generateGroupKey();
|
|
1276
|
-
const newEpoch = keyring.currentEpoch + 1;
|
|
1277
|
-
const wrappedKeys = {};
|
|
1278
|
-
for (const [memberId, memberPublicKey] of Object.entries(remainingMembers)) {
|
|
1279
|
-
wrappedKeys[memberId] = await wrapGroupKey(resolvedGek, memberPublicKey, adminKeyPair.privateKey);
|
|
1280
|
-
}
|
|
1281
|
-
const newKeyring = {
|
|
1282
|
-
currentEpoch: newEpoch,
|
|
1283
|
-
epochs: {
|
|
1284
|
-
...keyring.epochs,
|
|
1285
|
-
[String(newEpoch)]: { adminPublicKey: adminKeyPair.publicKey, wrappedKeys }
|
|
1286
|
-
}
|
|
1287
|
-
};
|
|
1288
|
-
return { keyring: newKeyring, gek: resolvedGek };
|
|
1289
|
-
}
|
|
1290
|
-
async function createGroupEncryptor(keyring, myIdentity, myPrivateKey) {
|
|
1291
|
-
const epochEncryptors = /* @__PURE__ */ new Map();
|
|
1292
|
-
for (const [epochStr, epochKeyring] of Object.entries(keyring.epochs)) {
|
|
1293
|
-
const epoch = parseInt(epochStr, 10);
|
|
1294
|
-
const wrapped = epochKeyring.wrappedKeys[myIdentity];
|
|
1295
|
-
if (!wrapped) continue;
|
|
1296
|
-
const gek = await unwrapGroupKey(wrapped, myPrivateKey, epochKeyring.adminPublicKey);
|
|
1297
|
-
epochEncryptors.set(epoch, createEncryptor(gek, `epoch-${epoch}`, GROUP_DATA_INFO));
|
|
1298
|
-
}
|
|
1299
|
-
const currentEpoch = keyring.currentEpoch;
|
|
1300
|
-
const currentEncryptor = epochEncryptors.get(currentEpoch);
|
|
1301
|
-
if (!currentEncryptor) {
|
|
1302
|
-
throw new Error(
|
|
1303
|
-
`No wrapped key found for identity "${myIdentity}" in epoch ${currentEpoch}. Ensure the admin has added this member to the keyring.`
|
|
1304
|
-
);
|
|
1305
|
-
}
|
|
1306
|
-
return {
|
|
1307
|
-
async encrypt(data) {
|
|
1308
|
-
const encrypted = await currentEncryptor.encrypt(data);
|
|
1309
|
-
return { ...encrypted, _epoch: currentEpoch };
|
|
1310
|
-
},
|
|
1311
|
-
async decrypt(wrapper) {
|
|
1312
|
-
const epoch = typeof wrapper._epoch === "number" ? wrapper._epoch : currentEpoch;
|
|
1313
|
-
const encryptor = epochEncryptors.get(epoch);
|
|
1314
|
-
if (!encryptor) {
|
|
1315
|
-
throw new Error(
|
|
1316
|
-
`No key available for epoch ${epoch}. This document was encrypted in a different epoch. Ensure your keyring is up to date.`
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
return encryptor.decrypt(wrapper);
|
|
1320
|
-
}
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
// src/entitlements.ts
|
|
1325
|
-
async function pullEntitlements(client, userId, opts) {
|
|
1326
|
-
const path = (opts?.path ?? "/pull/users/{userId}/entitlements").replace("{userId}", userId);
|
|
1327
|
-
const field = opts?.field ?? "features";
|
|
1328
|
-
try {
|
|
1329
|
-
const result = await client.pull(path);
|
|
1330
|
-
const list = result.data?.[field];
|
|
1331
|
-
if (!Array.isArray(list)) return [];
|
|
1332
|
-
return list.filter((s) => typeof s === "string");
|
|
1333
|
-
} catch (err) {
|
|
1334
|
-
if (err instanceof StarfishHttpError && err.status === 404) return [];
|
|
1335
|
-
throw err;
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
1226
|
export {
|
|
1339
1227
|
AbortError,
|
|
1340
1228
|
ConflictError,
|
|
@@ -1344,7 +1232,7 @@ export {
|
|
|
1344
1232
|
StarfishHttpError,
|
|
1345
1233
|
SyncManager,
|
|
1346
1234
|
ValidationError,
|
|
1347
|
-
|
|
1235
|
+
buildRevocationList,
|
|
1348
1236
|
classifyError,
|
|
1349
1237
|
computeHash,
|
|
1350
1238
|
configurePlatform,
|
|
@@ -1352,9 +1240,6 @@ export {
|
|
|
1352
1240
|
createDebouncedPush,
|
|
1353
1241
|
createDebouncedSync,
|
|
1354
1242
|
createDedupFetch,
|
|
1355
|
-
createEncryptor,
|
|
1356
|
-
createGroupEncryptor,
|
|
1357
|
-
createGroupKeyring,
|
|
1358
1243
|
createIndexedDBStorage,
|
|
1359
1244
|
createMetricsCollector,
|
|
1360
1245
|
createMigrator,
|
|
@@ -1364,27 +1249,22 @@ export {
|
|
|
1364
1249
|
createSoftDeleteResolver,
|
|
1365
1250
|
createSuspenseResource,
|
|
1366
1251
|
createUnionMerge,
|
|
1367
|
-
deriveGroupKeyPair,
|
|
1368
1252
|
exportData,
|
|
1369
1253
|
exportToBlob,
|
|
1370
1254
|
fetchServerConfig,
|
|
1371
|
-
generateGroupKey,
|
|
1372
1255
|
importData,
|
|
1373
1256
|
isBackgroundSyncSupported,
|
|
1374
1257
|
isServiceWorkerSupported,
|
|
1375
1258
|
noopSyncLogger,
|
|
1376
1259
|
pruneTombstones,
|
|
1377
|
-
pullEntitlements,
|
|
1378
1260
|
registerBackgroundSync,
|
|
1379
1261
|
registerServiceWorker,
|
|
1380
|
-
|
|
1381
|
-
|
|
1262
|
+
revocationListCanonicalSigningInput,
|
|
1263
|
+
stableStringify3 as stableStringify,
|
|
1382
1264
|
startAdaptivePolling,
|
|
1383
1265
|
startPolling,
|
|
1384
1266
|
timestampWinner,
|
|
1385
1267
|
unregisterServiceWorkers,
|
|
1386
|
-
|
|
1387
|
-
withConflictMeta,
|
|
1388
|
-
wrapGroupKey
|
|
1268
|
+
withConflictMeta
|
|
1389
1269
|
};
|
|
1390
1270
|
//# sourceMappingURL=index.js.map
|