@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/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 stableStringify3, computeHash } from "@drakkar.software/starfish-protocol";
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
- const { cap, devEdPrivHex, pubHex, presenterAlg } = await this.capProvider.getCap();
103
- const req = {
104
- method,
105
- pathAndQuery,
106
- body,
107
- host: this.signingHost()
108
- };
109
- const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
110
- const { alg, sig, ts, nonce } = await signRequest(req, devEdPrivHex, {
111
- alg: signAlg
112
- });
113
- const headers = {
114
- Authorization: `Cap ${encodeCapAuth(cap)}`,
115
- "X-Starfish-Sig": sig,
116
- "X-Starfish-Ts": String(ts),
117
- "X-Starfish-Nonce": nonce,
118
- "X-Starfish-Alg": alg
119
- };
120
- if (pubHex !== void 0) headers["X-Starfish-Pub"] = pubHex;
121
- return headers;
122
- }
123
- return {};
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: { Accept: "application/json", ...authHeaders }
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 fields (`authorPubkey` + `authorSignature`) live inside `data`
177
- * and are produced by `SyncManager` when a `signer` is configured.
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
- "Content-Type": "application/json",
191
- Accept: "application/json",
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 authHeaders = await this.buildAuthHeaders("POST", sendPath, body);
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
- "Content-Type": "application/json",
233
- Accept: "application/json",
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: { Accept: "*/*", ...authHeaders }
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("Content-Type") ?? "application/octet-stream";
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
- "Content-Type": contentType,
273
- Accept: "application/json",
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 { deepMerge, getBase64, stableStringify as stableStringify2 } from "@drakkar.software/starfish-protocol";
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 payload = sealed;
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 canonical = stableStringify2(sealed);
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
- payload = {
406
- ...sealed,
407
- authorPubkey: devEdPubHex,
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
- payload,
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
- stableStringify3 as stableStringify,
1399
+ stableStringify2 as stableStringify,
1329
1400
  startAdaptivePolling,
1330
1401
  startPolling,
1331
1402
  timestampWinner,