@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/bindings/zustand.js
CHANGED
|
@@ -230,7 +230,22 @@ import { useEffect, useRef, useState, useCallback } from "react";
|
|
|
230
230
|
|
|
231
231
|
// src/client.ts
|
|
232
232
|
import {
|
|
233
|
+
AUTHOR_PUBKEY_FIELD,
|
|
234
|
+
AUTHOR_SIGNATURE_FIELD,
|
|
235
|
+
DATA_FIELD,
|
|
236
|
+
TS_FIELD,
|
|
237
|
+
BASE_HASH_FIELD,
|
|
238
|
+
PUSH_PATH_PREFIX,
|
|
239
|
+
HEADER_AUTHORIZATION,
|
|
240
|
+
HEADER_SIG,
|
|
241
|
+
HEADER_TS,
|
|
242
|
+
HEADER_NONCE,
|
|
243
|
+
HEADER_ALG,
|
|
244
|
+
HEADER_PUB,
|
|
245
|
+
HEADER_CONTENT_TYPE,
|
|
246
|
+
HEADER_ACCEPT,
|
|
233
247
|
DEFAULT_ALG,
|
|
248
|
+
signAppendAuthor,
|
|
234
249
|
signRequest,
|
|
235
250
|
stableStringify
|
|
236
251
|
} from "@drakkar.software/starfish-protocol";
|
|
@@ -253,6 +268,9 @@ var StarfishHttpError = class extends Error {
|
|
|
253
268
|
|
|
254
269
|
// src/client.ts
|
|
255
270
|
var APPEND_DEFAULT_FIELD = "items";
|
|
271
|
+
function stripPushPrefix(path) {
|
|
272
|
+
return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path;
|
|
273
|
+
}
|
|
256
274
|
function encodeCapAuth(cap) {
|
|
257
275
|
const json = stableStringify(cap);
|
|
258
276
|
if (typeof btoa === "function") {
|
|
@@ -323,29 +341,54 @@ var StarfishClient = class {
|
|
|
323
341
|
* The host bound into the signature is derived from `baseUrl` once per call.
|
|
324
342
|
*/
|
|
325
343
|
async buildAuthHeaders(method, pathAndQuery, body) {
|
|
326
|
-
if (this.capProvider) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
344
|
+
if (!this.capProvider) return {};
|
|
345
|
+
const capCtx = await this.capProvider.getCap();
|
|
346
|
+
return this.capRequestHeaders(capCtx, method, pathAndQuery, body);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Build the request-signing headers from an ALREADY-fetched cap context. Split
|
|
350
|
+
* out of {@link buildAuthHeaders} so {@link append} can fetch the cap once and
|
|
351
|
+
* reuse it for BOTH the author signature (over the element data) and the
|
|
352
|
+
* request signature (over the body), without redeeming the cap twice — a
|
|
353
|
+
* second `getCap()` could rotate keys and break the `authorPubkey ===
|
|
354
|
+
* presenter` bind the server checks.
|
|
355
|
+
*/
|
|
356
|
+
async capRequestHeaders(capCtx, method, pathAndQuery, body) {
|
|
357
|
+
const { cap, devEdPrivHex, pubHex, presenterAlg } = capCtx;
|
|
358
|
+
const req = {
|
|
359
|
+
method,
|
|
360
|
+
pathAndQuery,
|
|
361
|
+
body,
|
|
362
|
+
host: this.signingHost()
|
|
363
|
+
};
|
|
364
|
+
const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
|
|
365
|
+
const { alg, sig, ts, nonce } = await signRequest(req, devEdPrivHex, {
|
|
366
|
+
alg: signAlg
|
|
367
|
+
});
|
|
368
|
+
const headers = {
|
|
369
|
+
[HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,
|
|
370
|
+
[HEADER_SIG]: sig,
|
|
371
|
+
[HEADER_TS]: String(ts),
|
|
372
|
+
[HEADER_NONCE]: nonce,
|
|
373
|
+
[HEADER_ALG]: alg
|
|
374
|
+
};
|
|
375
|
+
if (pubHex !== void 0) headers[HEADER_PUB] = pubHex;
|
|
376
|
+
return headers;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Resolve the author public key to attach to a signed append: the redeemer's
|
|
380
|
+
* `pubHex` for an audience cap, else the cert subject `cap.sub` for a
|
|
381
|
+
* device/member cap. This is the SAME key that signs the request, so a server
|
|
382
|
+
* enforcing author proof can bind the stored element to its writer. Returns
|
|
383
|
+
* undefined only for a (malformed) cap with neither — the append then goes
|
|
384
|
+
* unsigned and a server requiring signatures rejects it.
|
|
385
|
+
*/
|
|
386
|
+
appendAuthorKey(capCtx) {
|
|
387
|
+
const { cap, pubHex, presenterAlg } = capCtx;
|
|
388
|
+
const authorPubHex = pubHex ?? cap.sub;
|
|
389
|
+
if (authorPubHex === void 0) return null;
|
|
390
|
+
const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
|
|
391
|
+
return { authorPubHex, signAlg };
|
|
349
392
|
}
|
|
350
393
|
async pull(path, checkpointOrOptions) {
|
|
351
394
|
let pathAndQuery = this.applyNamespace(path);
|
|
@@ -380,7 +423,7 @@ var StarfishClient = class {
|
|
|
380
423
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
381
424
|
const res = await this.fetch(url, {
|
|
382
425
|
method: "GET",
|
|
383
|
-
headers: {
|
|
426
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
384
427
|
});
|
|
385
428
|
if (!res.ok) {
|
|
386
429
|
throw new StarfishHttpError(res.status, await res.text());
|
|
@@ -398,22 +441,27 @@ var StarfishClient = class {
|
|
|
398
441
|
* @param data - The full document data to push
|
|
399
442
|
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
400
443
|
*
|
|
401
|
-
* v3 author
|
|
402
|
-
*
|
|
444
|
+
* v3 author proof (`authorPubkey` + `authorSignature`) is passed via `author`
|
|
445
|
+
* (produced by `SyncManager` when a `signer` is configured) and sent as
|
|
446
|
+
* top-level body siblings of `data`, where the server verifies it.
|
|
403
447
|
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
404
448
|
*/
|
|
405
|
-
async push(path, data, baseHash) {
|
|
449
|
+
async push(path, data, baseHash, author) {
|
|
406
450
|
const body = JSON.stringify({
|
|
407
|
-
data,
|
|
408
|
-
baseHash
|
|
451
|
+
[DATA_FIELD]: data,
|
|
452
|
+
[BASE_HASH_FIELD]: baseHash,
|
|
453
|
+
...author && {
|
|
454
|
+
[AUTHOR_PUBKEY_FIELD]: author.authorPubkey,
|
|
455
|
+
[AUTHOR_SIGNATURE_FIELD]: author.authorSignature
|
|
456
|
+
}
|
|
409
457
|
});
|
|
410
458
|
const sendPath = this.applyNamespace(path);
|
|
411
459
|
const authHeaders = await this.buildAuthHeaders("POST", sendPath, body);
|
|
412
460
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
413
461
|
method: "POST",
|
|
414
462
|
headers: {
|
|
415
|
-
|
|
416
|
-
|
|
463
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
464
|
+
[HEADER_ACCEPT]: "application/json",
|
|
417
465
|
...authHeaders
|
|
418
466
|
},
|
|
419
467
|
body
|
|
@@ -446,16 +494,32 @@ var StarfishClient = class {
|
|
|
446
494
|
* cap is reached (partition by a path parameter for higher volume).
|
|
447
495
|
*/
|
|
448
496
|
async append(path, data, opts = {}) {
|
|
449
|
-
const bodyObj = { data };
|
|
450
|
-
if (opts.ts !== void 0) bodyObj["ts"] = opts.ts;
|
|
451
|
-
const body = JSON.stringify(bodyObj);
|
|
452
497
|
const sendPath = this.applyNamespace(path);
|
|
453
|
-
const
|
|
498
|
+
const bodyObj = { [DATA_FIELD]: data };
|
|
499
|
+
if (opts.ts !== void 0) bodyObj[TS_FIELD] = opts.ts;
|
|
500
|
+
const capCtx = this.capProvider ? await this.capProvider.getCap() : null;
|
|
501
|
+
if (capCtx) {
|
|
502
|
+
const authorKey = this.appendAuthorKey(capCtx);
|
|
503
|
+
if (authorKey) {
|
|
504
|
+
const documentKey = stripPushPrefix(path);
|
|
505
|
+
const { authorPubkey, authorSignature } = signAppendAuthor(
|
|
506
|
+
documentKey,
|
|
507
|
+
data,
|
|
508
|
+
authorKey.authorPubHex,
|
|
509
|
+
capCtx.devEdPrivHex,
|
|
510
|
+
authorKey.signAlg
|
|
511
|
+
);
|
|
512
|
+
bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey;
|
|
513
|
+
bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const body = JSON.stringify(bodyObj);
|
|
517
|
+
const authHeaders = capCtx ? await this.capRequestHeaders(capCtx, "POST", sendPath, body) : {};
|
|
454
518
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
455
519
|
method: "POST",
|
|
456
520
|
headers: {
|
|
457
|
-
|
|
458
|
-
|
|
521
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
522
|
+
[HEADER_ACCEPT]: "application/json",
|
|
459
523
|
...authHeaders
|
|
460
524
|
},
|
|
461
525
|
body
|
|
@@ -474,13 +538,13 @@ var StarfishClient = class {
|
|
|
474
538
|
const authHeaders = await this.buildAuthHeaders("GET", sendPath, void 0);
|
|
475
539
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
476
540
|
method: "GET",
|
|
477
|
-
headers: {
|
|
541
|
+
headers: { [HEADER_ACCEPT]: "*/*", ...authHeaders }
|
|
478
542
|
});
|
|
479
543
|
if (!res.ok) {
|
|
480
544
|
throw new StarfishHttpError(res.status, await res.text());
|
|
481
545
|
}
|
|
482
546
|
const etag = res.headers.get("ETag")?.replace(/"/g, "") ?? null;
|
|
483
|
-
const contentType = res.headers.get(
|
|
547
|
+
const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? "application/octet-stream";
|
|
484
548
|
const data = await res.arrayBuffer();
|
|
485
549
|
return { data, hash: etag, contentType };
|
|
486
550
|
}
|
|
@@ -494,8 +558,8 @@ var StarfishClient = class {
|
|
|
494
558
|
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
495
559
|
method: "POST",
|
|
496
560
|
headers: {
|
|
497
|
-
|
|
498
|
-
|
|
561
|
+
[HEADER_CONTENT_TYPE]: contentType,
|
|
562
|
+
[HEADER_ACCEPT]: "application/json",
|
|
499
563
|
...authHeaders
|
|
500
564
|
},
|
|
501
565
|
body: data
|
|
@@ -508,7 +572,13 @@ var StarfishClient = class {
|
|
|
508
572
|
};
|
|
509
573
|
|
|
510
574
|
// src/sync.ts
|
|
511
|
-
import {
|
|
575
|
+
import {
|
|
576
|
+
AUTHOR_PUBKEY_FIELD as AUTHOR_PUBKEY_FIELD2,
|
|
577
|
+
AUTHOR_SIGNATURE_FIELD as AUTHOR_SIGNATURE_FIELD2,
|
|
578
|
+
deepMerge,
|
|
579
|
+
docAuthorCanonicalInput,
|
|
580
|
+
getBase64
|
|
581
|
+
} from "@drakkar.software/starfish-protocol";
|
|
512
582
|
|
|
513
583
|
// src/validate.ts
|
|
514
584
|
var ValidationError = class extends Error {
|
|
@@ -613,23 +683,24 @@ var SyncManager = class {
|
|
|
613
683
|
try {
|
|
614
684
|
const sealed = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
615
685
|
if (this.aborted) throw new AbortError();
|
|
616
|
-
let
|
|
686
|
+
let author;
|
|
617
687
|
if (this.signer) {
|
|
618
688
|
const { devEdPubHex, sign } = await this.signer.getSigner();
|
|
619
689
|
if (this.aborted) throw new AbortError();
|
|
620
|
-
const
|
|
690
|
+
const documentKey = stripPushPrefix(this.pushPath);
|
|
691
|
+
const canonical = docAuthorCanonicalInput(documentKey, sealed);
|
|
621
692
|
const sigBytes = await sign(new TextEncoder().encode(canonical));
|
|
622
693
|
if (this.aborted) throw new AbortError();
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
authorSignature: getBase64().encode(sigBytes)
|
|
694
|
+
author = {
|
|
695
|
+
[AUTHOR_PUBKEY_FIELD2]: devEdPubHex,
|
|
696
|
+
[AUTHOR_SIGNATURE_FIELD2]: getBase64().encode(sigBytes)
|
|
627
697
|
};
|
|
628
698
|
}
|
|
629
699
|
const result = await this.client.push(
|
|
630
700
|
this.pushPath,
|
|
631
|
-
|
|
632
|
-
this.lastHash
|
|
701
|
+
sealed,
|
|
702
|
+
this.lastHash,
|
|
703
|
+
author
|
|
633
704
|
);
|
|
634
705
|
if (this.aborted) throw new AbortError();
|
|
635
706
|
this.lastHash = result.hash;
|