@drakkar.software/starfish-client 2.3.0 → 3.0.0-alpha.1
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 +127 -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 +133 -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 +55 -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,111 @@ 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, pubHex } = 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
|
+
const headers = {
|
|
94
|
+
Authorization: `Cap ${encodeCapAuth(cap)}`,
|
|
95
|
+
"X-Starfish-Sig": sig,
|
|
96
|
+
"X-Starfish-Ts": String(ts),
|
|
97
|
+
"X-Starfish-Nonce": nonce
|
|
98
|
+
};
|
|
99
|
+
if (pubHex !== void 0) headers["X-Starfish-Pub"] = pubHex;
|
|
100
|
+
return headers;
|
|
101
|
+
}
|
|
102
|
+
return {};
|
|
31
103
|
}
|
|
32
104
|
async pull(path, checkpointOrOptions) {
|
|
33
|
-
let
|
|
105
|
+
let pathAndQuery = path;
|
|
34
106
|
let appendField;
|
|
35
107
|
if (typeof checkpointOrOptions === "number") {
|
|
36
|
-
if (checkpointOrOptions)
|
|
108
|
+
if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`;
|
|
37
109
|
} else if (checkpointOrOptions != null) {
|
|
38
|
-
|
|
110
|
+
const opts = checkpointOrOptions;
|
|
111
|
+
const isPullOptions = opts.withKeyring !== void 0 || opts.checkpoint !== void 0;
|
|
39
112
|
const params = new URLSearchParams();
|
|
40
|
-
if (
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
113
|
+
if (isPullOptions) {
|
|
114
|
+
if (opts.checkpoint != null && opts.checkpoint > 0) {
|
|
115
|
+
params.set("checkpoint", String(opts.checkpoint));
|
|
116
|
+
}
|
|
117
|
+
if (opts.withKeyring) {
|
|
118
|
+
params.set("withKeyring", "1");
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
appendField = opts.appendField ?? APPEND_DEFAULT_FIELD;
|
|
122
|
+
if (opts.since != null) {
|
|
123
|
+
if (opts.since < 0) throw new Error("since must be non-negative");
|
|
124
|
+
params.set("checkpoint", String(opts.since));
|
|
125
|
+
}
|
|
126
|
+
if (opts.last != null) {
|
|
127
|
+
if (opts.last < 0) throw new Error("last must be non-negative");
|
|
128
|
+
params.set("last", String(opts.last));
|
|
129
|
+
}
|
|
47
130
|
}
|
|
48
|
-
if (params.size > 0)
|
|
131
|
+
if (params.size > 0) pathAndQuery += `?${params.toString()}`;
|
|
49
132
|
}
|
|
50
|
-
const
|
|
133
|
+
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
134
|
+
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
51
135
|
const res = await this.fetch(url, {
|
|
52
136
|
method: "GET",
|
|
53
137
|
headers: { Accept: "application/json", ...authHeaders }
|
|
@@ -67,16 +151,17 @@ var StarfishClient = class {
|
|
|
67
151
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
68
152
|
* @param data - The full document data to push
|
|
69
153
|
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
70
|
-
*
|
|
154
|
+
*
|
|
155
|
+
* v3 author fields (`authorPubkey` + `authorSignature`) live inside `data`
|
|
156
|
+
* and are produced by `SyncManager` when a `signer` is configured.
|
|
71
157
|
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
72
158
|
*/
|
|
73
|
-
async push(path, data, baseHash
|
|
159
|
+
async push(path, data, baseHash) {
|
|
74
160
|
const body = JSON.stringify({
|
|
75
161
|
data,
|
|
76
|
-
baseHash
|
|
77
|
-
...authorSignature && { authorSignature }
|
|
162
|
+
baseHash
|
|
78
163
|
});
|
|
79
|
-
const authHeaders =
|
|
164
|
+
const authHeaders = await this.buildAuthHeaders("POST", path, body);
|
|
80
165
|
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
81
166
|
method: "POST",
|
|
82
167
|
headers: {
|
|
@@ -99,7 +184,7 @@ var StarfishClient = class {
|
|
|
99
184
|
* Returns raw bytes with the content hash from the ETag header.
|
|
100
185
|
*/
|
|
101
186
|
async pullBlob(path) {
|
|
102
|
-
const authHeaders =
|
|
187
|
+
const authHeaders = await this.buildAuthHeaders("GET", path, void 0);
|
|
103
188
|
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
104
189
|
method: "GET",
|
|
105
190
|
headers: { Accept: "*/*", ...authHeaders }
|
|
@@ -117,7 +202,7 @@ var StarfishClient = class {
|
|
|
117
202
|
* Binary collections use last-write-wins (no conflict detection).
|
|
118
203
|
*/
|
|
119
204
|
async pushBlob(path, data, contentType) {
|
|
120
|
-
const authHeaders =
|
|
205
|
+
const authHeaders = await this.buildAuthHeaders("POST", path, void 0);
|
|
121
206
|
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
122
207
|
method: "POST",
|
|
123
208
|
headers: {
|
|
@@ -135,51 +220,7 @@ var StarfishClient = class {
|
|
|
135
220
|
};
|
|
136
221
|
|
|
137
222
|
// 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
|
-
}
|
|
223
|
+
import { deepMerge, getBase64, stableStringify as stableStringify2 } from "@drakkar.software/starfish-protocol";
|
|
183
224
|
|
|
184
225
|
// src/validate.ts
|
|
185
226
|
var ValidationError = class extends Error {
|
|
@@ -211,7 +252,7 @@ var SyncManager = class {
|
|
|
211
252
|
onConflict;
|
|
212
253
|
maxRetries;
|
|
213
254
|
encryptor;
|
|
214
|
-
|
|
255
|
+
signer;
|
|
215
256
|
logger;
|
|
216
257
|
loggerName;
|
|
217
258
|
validate;
|
|
@@ -225,11 +266,11 @@ var SyncManager = class {
|
|
|
225
266
|
this.pushPath = options.pushPath;
|
|
226
267
|
this.onConflict = options.onConflict ?? deepMerge;
|
|
227
268
|
this.maxRetries = options.maxRetries ?? 3;
|
|
228
|
-
this.
|
|
269
|
+
this.signer = options.signer;
|
|
229
270
|
this.logger = options.logger;
|
|
230
271
|
this.loggerName = options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
|
|
231
272
|
this.validate = options.validate;
|
|
232
|
-
this.encryptor = options.encryptor ??
|
|
273
|
+
this.encryptor = options.encryptor ?? null;
|
|
233
274
|
}
|
|
234
275
|
abort() {
|
|
235
276
|
this.aborted = true;
|
|
@@ -289,15 +330,25 @@ var SyncManager = class {
|
|
|
289
330
|
let pendingData = data;
|
|
290
331
|
while (attempt <= this.maxRetries) {
|
|
291
332
|
try {
|
|
292
|
-
const
|
|
293
|
-
if (this.aborted) throw new AbortError();
|
|
294
|
-
const sig = this.signData ? await this.signData(stableStringify(payload)) : void 0;
|
|
333
|
+
const sealed = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
295
334
|
if (this.aborted) throw new AbortError();
|
|
335
|
+
let payload = sealed;
|
|
336
|
+
if (this.signer) {
|
|
337
|
+
const { devEdPubHex, sign } = await this.signer.getSigner();
|
|
338
|
+
if (this.aborted) throw new AbortError();
|
|
339
|
+
const canonical = stableStringify2(sealed);
|
|
340
|
+
const sigBytes = await sign(new TextEncoder().encode(canonical));
|
|
341
|
+
if (this.aborted) throw new AbortError();
|
|
342
|
+
payload = {
|
|
343
|
+
...sealed,
|
|
344
|
+
authorPubkey: devEdPubHex,
|
|
345
|
+
authorSignature: getBase64().encode(sigBytes)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
296
348
|
const result = await this.client.push(
|
|
297
349
|
this.pushPath,
|
|
298
350
|
payload,
|
|
299
|
-
this.lastHash
|
|
300
|
-
sig
|
|
351
|
+
this.lastHash
|
|
301
352
|
);
|
|
302
353
|
if (this.aborted) throw new AbortError();
|
|
303
354
|
this.lastHash = result.hash;
|
|
@@ -339,6 +390,9 @@ var SyncManager = class {
|
|
|
339
390
|
}
|
|
340
391
|
};
|
|
341
392
|
|
|
393
|
+
// src/index.ts
|
|
394
|
+
import { ENCRYPTED_KEY } from "@drakkar.software/starfish-protocol";
|
|
395
|
+
|
|
342
396
|
// src/logger.ts
|
|
343
397
|
var consoleSyncLogger = {
|
|
344
398
|
pullStart: (s) => console.log(`[starfish:${s}] pull started`),
|
|
@@ -1171,170 +1225,6 @@ function createMultiStoreSync(options) {
|
|
|
1171
1225
|
}
|
|
1172
1226
|
return { serialize, restore, version };
|
|
1173
1227
|
}
|
|
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
1228
|
export {
|
|
1339
1229
|
AbortError,
|
|
1340
1230
|
ConflictError,
|
|
@@ -1344,7 +1234,7 @@ export {
|
|
|
1344
1234
|
StarfishHttpError,
|
|
1345
1235
|
SyncManager,
|
|
1346
1236
|
ValidationError,
|
|
1347
|
-
|
|
1237
|
+
buildRevocationList,
|
|
1348
1238
|
classifyError,
|
|
1349
1239
|
computeHash,
|
|
1350
1240
|
configurePlatform,
|
|
@@ -1352,9 +1242,6 @@ export {
|
|
|
1352
1242
|
createDebouncedPush,
|
|
1353
1243
|
createDebouncedSync,
|
|
1354
1244
|
createDedupFetch,
|
|
1355
|
-
createEncryptor,
|
|
1356
|
-
createGroupEncryptor,
|
|
1357
|
-
createGroupKeyring,
|
|
1358
1245
|
createIndexedDBStorage,
|
|
1359
1246
|
createMetricsCollector,
|
|
1360
1247
|
createMigrator,
|
|
@@ -1364,27 +1251,22 @@ export {
|
|
|
1364
1251
|
createSoftDeleteResolver,
|
|
1365
1252
|
createSuspenseResource,
|
|
1366
1253
|
createUnionMerge,
|
|
1367
|
-
deriveGroupKeyPair,
|
|
1368
1254
|
exportData,
|
|
1369
1255
|
exportToBlob,
|
|
1370
1256
|
fetchServerConfig,
|
|
1371
|
-
generateGroupKey,
|
|
1372
1257
|
importData,
|
|
1373
1258
|
isBackgroundSyncSupported,
|
|
1374
1259
|
isServiceWorkerSupported,
|
|
1375
1260
|
noopSyncLogger,
|
|
1376
1261
|
pruneTombstones,
|
|
1377
|
-
pullEntitlements,
|
|
1378
1262
|
registerBackgroundSync,
|
|
1379
1263
|
registerServiceWorker,
|
|
1380
|
-
|
|
1381
|
-
|
|
1264
|
+
revocationListCanonicalSigningInput,
|
|
1265
|
+
stableStringify3 as stableStringify,
|
|
1382
1266
|
startAdaptivePolling,
|
|
1383
1267
|
startPolling,
|
|
1384
1268
|
timestampWinner,
|
|
1385
1269
|
unregisterServiceWorkers,
|
|
1386
|
-
|
|
1387
|
-
withConflictMeta,
|
|
1388
|
-
wrapGroupKey
|
|
1270
|
+
withConflictMeta
|
|
1389
1271
|
};
|
|
1390
1272
|
//# sourceMappingURL=index.js.map
|