@drakkar.software/starfish-client 3.0.0-alpha.6 → 3.0.0-alpha.8
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/bindings/zustand.js +121 -50
- package/dist/bindings/zustand.js.map +3 -3
- package/dist/client.d.ts +27 -3
- package/dist/index.js +123 -52
- package/dist/index.js.map +3 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { configurePlatform } from "@drakkar.software/starfish-protocol";
|
|
3
|
-
import { stableStringify as
|
|
3
|
+
import { stableStringify as stableStringify2, computeHash } from "@drakkar.software/starfish-protocol";
|
|
4
4
|
import { buildRevocationList, revocationListCanonicalSigningInput } from "@drakkar.software/starfish-protocol";
|
|
5
5
|
|
|
6
6
|
// src/client.ts
|
|
7
7
|
import {
|
|
8
|
+
AUTHOR_PUBKEY_FIELD,
|
|
9
|
+
AUTHOR_SIGNATURE_FIELD,
|
|
10
|
+
DATA_FIELD,
|
|
11
|
+
TS_FIELD,
|
|
12
|
+
BASE_HASH_FIELD,
|
|
13
|
+
PUSH_PATH_PREFIX,
|
|
14
|
+
HEADER_AUTHORIZATION,
|
|
15
|
+
HEADER_SIG,
|
|
16
|
+
HEADER_TS,
|
|
17
|
+
HEADER_NONCE,
|
|
18
|
+
HEADER_ALG,
|
|
19
|
+
HEADER_PUB,
|
|
20
|
+
HEADER_CONTENT_TYPE,
|
|
21
|
+
HEADER_ACCEPT,
|
|
8
22
|
DEFAULT_ALG,
|
|
23
|
+
signAppendAuthor,
|
|
9
24
|
signRequest,
|
|
10
25
|
stableStringify
|
|
11
26
|
} from "@drakkar.software/starfish-protocol";
|
|
@@ -28,6 +43,9 @@ var StarfishHttpError = class extends Error {
|
|
|
28
43
|
|
|
29
44
|
// src/client.ts
|
|
30
45
|
var APPEND_DEFAULT_FIELD = "items";
|
|
46
|
+
function stripPushPrefix(path) {
|
|
47
|
+
return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path;
|
|
48
|
+
}
|
|
31
49
|
function encodeCapAuth(cap) {
|
|
32
50
|
const json = stableStringify(cap);
|
|
33
51
|
if (typeof btoa === "function") {
|
|
@@ -98,29 +116,54 @@ var StarfishClient = class {
|
|
|
98
116
|
* The host bound into the signature is derived from `baseUrl` once per call.
|
|
99
117
|
*/
|
|
100
118
|
async buildAuthHeaders(method, pathAndQuery, body) {
|
|
101
|
-
if (this.capProvider) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
119
|
+
if (!this.capProvider) return {};
|
|
120
|
+
const capCtx = await this.capProvider.getCap();
|
|
121
|
+
return this.capRequestHeaders(capCtx, method, pathAndQuery, body);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build the request-signing headers from an ALREADY-fetched cap context. Split
|
|
125
|
+
* out of {@link buildAuthHeaders} so {@link append} can fetch the cap once and
|
|
126
|
+
* reuse it for BOTH the author signature (over the element data) and the
|
|
127
|
+
* request signature (over the body), without redeeming the cap twice — a
|
|
128
|
+
* second `getCap()` could rotate keys and break the `authorPubkey ===
|
|
129
|
+
* presenter` bind the server checks.
|
|
130
|
+
*/
|
|
131
|
+
async capRequestHeaders(capCtx, method, pathAndQuery, body) {
|
|
132
|
+
const { cap, devEdPrivHex, pubHex, presenterAlg } = capCtx;
|
|
133
|
+
const req = {
|
|
134
|
+
method,
|
|
135
|
+
pathAndQuery,
|
|
136
|
+
body,
|
|
137
|
+
host: this.signingHost()
|
|
138
|
+
};
|
|
139
|
+
const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
|
|
140
|
+
const { alg, sig, ts, nonce } = await signRequest(req, devEdPrivHex, {
|
|
141
|
+
alg: signAlg
|
|
142
|
+
});
|
|
143
|
+
const headers = {
|
|
144
|
+
[HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,
|
|
145
|
+
[HEADER_SIG]: sig,
|
|
146
|
+
[HEADER_TS]: String(ts),
|
|
147
|
+
[HEADER_NONCE]: nonce,
|
|
148
|
+
[HEADER_ALG]: alg
|
|
149
|
+
};
|
|
150
|
+
if (pubHex !== void 0) headers[HEADER_PUB] = pubHex;
|
|
151
|
+
return headers;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the author public key to attach to a signed append: the redeemer's
|
|
155
|
+
* `pubHex` for an audience cap, else the cert subject `cap.sub` for a
|
|
156
|
+
* device/member cap. This is the SAME key that signs the request, so a server
|
|
157
|
+
* enforcing author proof can bind the stored element to its writer. Returns
|
|
158
|
+
* undefined only for a (malformed) cap with neither — the append then goes
|
|
159
|
+
* unsigned and a server requiring signatures rejects it.
|
|
160
|
+
*/
|
|
161
|
+
appendAuthorKey(capCtx) {
|
|
162
|
+
const { cap, pubHex, presenterAlg } = capCtx;
|
|
163
|
+
const authorPubHex = pubHex ?? cap.sub;
|
|
164
|
+
if (authorPubHex === void 0) return null;
|
|
165
|
+
const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
|
|
166
|
+
return { authorPubHex, signAlg };
|
|
124
167
|
}
|
|
125
168
|
async pull(path, checkpointOrOptions) {
|
|
126
169
|
let pathAndQuery = this.applyNamespace(path);
|
|
@@ -155,7 +198,7 @@ var StarfishClient = class {
|
|
|
155
198
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
156
199
|
const res = await this.fetch(url, {
|
|
157
200
|
method: "GET",
|
|
158
|
-
headers: {
|
|
201
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
159
202
|
});
|
|
160
203
|
if (!res.ok) {
|
|
161
204
|
throw new StarfishHttpError(res.status, await res.text());
|
|
@@ -173,22 +216,27 @@ var StarfishClient = class {
|
|
|
173
216
|
* @param data - The full document data to push
|
|
174
217
|
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
175
218
|
*
|
|
176
|
-
* v3 author
|
|
177
|
-
*
|
|
219
|
+
* v3 author proof (`authorPubkey` + `authorSignature`) is passed via `author`
|
|
220
|
+
* (produced by `SyncManager` when a `signer` is configured) and sent as
|
|
221
|
+
* top-level body siblings of `data`, where the server verifies it.
|
|
178
222
|
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
179
223
|
*/
|
|
180
|
-
async push(path, data, baseHash) {
|
|
224
|
+
async push(path, data, baseHash, author) {
|
|
181
225
|
const body = JSON.stringify({
|
|
182
|
-
data,
|
|
183
|
-
baseHash
|
|
226
|
+
[DATA_FIELD]: data,
|
|
227
|
+
[BASE_HASH_FIELD]: baseHash,
|
|
228
|
+
...author && {
|
|
229
|
+
[AUTHOR_PUBKEY_FIELD]: author.authorPubkey,
|
|
230
|
+
[AUTHOR_SIGNATURE_FIELD]: author.authorSignature
|
|
231
|
+
}
|
|
184
232
|
});
|
|
185
233
|
const sendPath = this.applyNamespace(path);
|
|
186
234
|
const authHeaders = await this.buildAuthHeaders("POST", sendPath, body);
|
|
187
235
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
188
236
|
method: "POST",
|
|
189
237
|
headers: {
|
|
190
|
-
|
|
191
|
-
|
|
238
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
239
|
+
[HEADER_ACCEPT]: "application/json",
|
|
192
240
|
...authHeaders
|
|
193
241
|
},
|
|
194
242
|
body
|
|
@@ -221,16 +269,32 @@ var StarfishClient = class {
|
|
|
221
269
|
* cap is reached (partition by a path parameter for higher volume).
|
|
222
270
|
*/
|
|
223
271
|
async append(path, data, opts = {}) {
|
|
224
|
-
const bodyObj = { data };
|
|
225
|
-
if (opts.ts !== void 0) bodyObj["ts"] = opts.ts;
|
|
226
|
-
const body = JSON.stringify(bodyObj);
|
|
227
272
|
const sendPath = this.applyNamespace(path);
|
|
228
|
-
const
|
|
273
|
+
const bodyObj = { [DATA_FIELD]: data };
|
|
274
|
+
if (opts.ts !== void 0) bodyObj[TS_FIELD] = opts.ts;
|
|
275
|
+
const capCtx = this.capProvider ? await this.capProvider.getCap() : null;
|
|
276
|
+
if (capCtx) {
|
|
277
|
+
const authorKey = this.appendAuthorKey(capCtx);
|
|
278
|
+
if (authorKey) {
|
|
279
|
+
const documentKey = stripPushPrefix(path);
|
|
280
|
+
const { authorPubkey, authorSignature } = signAppendAuthor(
|
|
281
|
+
documentKey,
|
|
282
|
+
data,
|
|
283
|
+
authorKey.authorPubHex,
|
|
284
|
+
capCtx.devEdPrivHex,
|
|
285
|
+
authorKey.signAlg
|
|
286
|
+
);
|
|
287
|
+
bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey;
|
|
288
|
+
bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const body = JSON.stringify(bodyObj);
|
|
292
|
+
const authHeaders = capCtx ? await this.capRequestHeaders(capCtx, "POST", sendPath, body) : {};
|
|
229
293
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
230
294
|
method: "POST",
|
|
231
295
|
headers: {
|
|
232
|
-
|
|
233
|
-
|
|
296
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
297
|
+
[HEADER_ACCEPT]: "application/json",
|
|
234
298
|
...authHeaders
|
|
235
299
|
},
|
|
236
300
|
body
|
|
@@ -249,13 +313,13 @@ var StarfishClient = class {
|
|
|
249
313
|
const authHeaders = await this.buildAuthHeaders("GET", sendPath, void 0);
|
|
250
314
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
251
315
|
method: "GET",
|
|
252
|
-
headers: {
|
|
316
|
+
headers: { [HEADER_ACCEPT]: "*/*", ...authHeaders }
|
|
253
317
|
});
|
|
254
318
|
if (!res.ok) {
|
|
255
319
|
throw new StarfishHttpError(res.status, await res.text());
|
|
256
320
|
}
|
|
257
321
|
const etag = res.headers.get("ETag")?.replace(/"/g, "") ?? null;
|
|
258
|
-
const contentType = res.headers.get(
|
|
322
|
+
const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? "application/octet-stream";
|
|
259
323
|
const data = await res.arrayBuffer();
|
|
260
324
|
return { data, hash: etag, contentType };
|
|
261
325
|
}
|
|
@@ -269,8 +333,8 @@ var StarfishClient = class {
|
|
|
269
333
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
270
334
|
method: "POST",
|
|
271
335
|
headers: {
|
|
272
|
-
|
|
273
|
-
|
|
336
|
+
[HEADER_CONTENT_TYPE]: contentType,
|
|
337
|
+
[HEADER_ACCEPT]: "application/json",
|
|
274
338
|
...authHeaders
|
|
275
339
|
},
|
|
276
340
|
body: data
|
|
@@ -283,7 +347,13 @@ var StarfishClient = class {
|
|
|
283
347
|
};
|
|
284
348
|
|
|
285
349
|
// src/sync.ts
|
|
286
|
-
import {
|
|
350
|
+
import {
|
|
351
|
+
AUTHOR_PUBKEY_FIELD as AUTHOR_PUBKEY_FIELD2,
|
|
352
|
+
AUTHOR_SIGNATURE_FIELD as AUTHOR_SIGNATURE_FIELD2,
|
|
353
|
+
deepMerge,
|
|
354
|
+
docAuthorCanonicalInput,
|
|
355
|
+
getBase64
|
|
356
|
+
} from "@drakkar.software/starfish-protocol";
|
|
287
357
|
|
|
288
358
|
// src/validate.ts
|
|
289
359
|
var ValidationError = class extends Error {
|
|
@@ -395,23 +465,24 @@ var SyncManager = class {
|
|
|
395
465
|
try {
|
|
396
466
|
const sealed = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
397
467
|
if (this.aborted) throw new AbortError();
|
|
398
|
-
let
|
|
468
|
+
let author;
|
|
399
469
|
if (this.signer) {
|
|
400
470
|
const { devEdPubHex, sign } = await this.signer.getSigner();
|
|
401
471
|
if (this.aborted) throw new AbortError();
|
|
402
|
-
const
|
|
472
|
+
const documentKey = stripPushPrefix(this.pushPath);
|
|
473
|
+
const canonical = docAuthorCanonicalInput(documentKey, sealed);
|
|
403
474
|
const sigBytes = await sign(new TextEncoder().encode(canonical));
|
|
404
475
|
if (this.aborted) throw new AbortError();
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
authorSignature: getBase64().encode(sigBytes)
|
|
476
|
+
author = {
|
|
477
|
+
[AUTHOR_PUBKEY_FIELD2]: devEdPubHex,
|
|
478
|
+
[AUTHOR_SIGNATURE_FIELD2]: getBase64().encode(sigBytes)
|
|
409
479
|
};
|
|
410
480
|
}
|
|
411
481
|
const result = await this.client.push(
|
|
412
482
|
this.pushPath,
|
|
413
|
-
|
|
414
|
-
this.lastHash
|
|
483
|
+
sealed,
|
|
484
|
+
this.lastHash,
|
|
485
|
+
author
|
|
415
486
|
);
|
|
416
487
|
if (this.aborted) throw new AbortError();
|
|
417
488
|
this.lastHash = result.hash;
|
|
@@ -1325,7 +1396,7 @@ export {
|
|
|
1325
1396
|
registerBackgroundSync,
|
|
1326
1397
|
registerServiceWorker,
|
|
1327
1398
|
revocationListCanonicalSigningInput,
|
|
1328
|
-
|
|
1399
|
+
stableStringify2 as stableStringify,
|
|
1329
1400
|
startAdaptivePolling,
|
|
1330
1401
|
startPolling,
|
|
1331
1402
|
timestampWinner,
|