@calimero-network/mero-js 19.24.0 → 19.24.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.
@@ -52,8 +52,12 @@ export declare class StaleTransportKeyError extends Error {
52
52
  export declare class SealedTransportError extends Error {
53
53
  status: number;
54
54
  code: string | undefined;
55
+ /** How long the node asked to wait before retrying (`Retry-After`), if it did. */
56
+ retryAfterMs?: number;
55
57
  name: string;
56
- constructor(status: number, code: string | undefined, message: string);
58
+ constructor(status: number, code: string | undefined, message: string,
59
+ /** How long the node asked to wait before retrying (`Retry-After`), if it did. */
60
+ retryAfterMs?: number);
57
61
  }
58
62
  /**
59
63
  * What the attest endpoint puts in report data bytes `32..64` when asked to
@@ -50,6 +50,14 @@ const HEADER_LEN = 1 + SESSION_ID_LEN + 8;
50
50
  const FRAME_HEAD = 0;
51
51
  const FRAME_DATA = 1;
52
52
  const FRAME_END = 2;
53
+ /**
54
+ * The largest frame the node sends: a kind byte, at most 64 KiB of data, a tag.
55
+ * Anything larger is refused before it is buffered, so a proxy cannot make the
56
+ * client hold an unbounded frame it has yet to authenticate.
57
+ */
58
+ const MAX_FRAME_LEN = 1 + 64 * 1024 + 16;
59
+ /** Far more than a handshake reply (69 bytes) ever is. */
60
+ const MAX_HANDSHAKE_REPLY = 1024;
53
61
  /** Open a new session this long before the node would drop the current one. */
54
62
  const RENEW_BEFORE_MS = 60000;
55
63
  const TRANSPORT_BINDING_DOMAIN = 'calimero.tee-attest.transport-key.v1';
@@ -64,10 +72,13 @@ export class StaleTransportKeyError extends Error {
64
72
  }
65
73
  /** The node refused the envelope itself, before anything inside it ran. */
66
74
  export class SealedTransportError extends Error {
67
- constructor(status, code, message) {
75
+ constructor(status, code, message,
76
+ /** How long the node asked to wait before retrying (`Retry-After`), if it did. */
77
+ retryAfterMs) {
68
78
  super(message);
69
79
  this.status = status;
70
80
  this.code = code;
81
+ this.retryAfterMs = retryAfterMs;
71
82
  this.name = 'SealedTransportError';
72
83
  }
73
84
  }
@@ -205,6 +216,19 @@ async function checkedKey(key) {
205
216
  return bytes;
206
217
  }
207
218
  async function handshake(baseFetch, nodeUrl, transportKey) {
219
+ try {
220
+ return await handshakeOnce(baseFetch, nodeUrl, transportKey);
221
+ }
222
+ catch (error) {
223
+ // The node limits how fast sessions open. A refused handshake opened
224
+ // nothing, so trying once more after the pause it asks for is safe.
225
+ if (!(error instanceof SealedTransportError) || error.code !== 'busy')
226
+ throw error;
227
+ await new Promise((resolve) => setTimeout(resolve, error.retryAfterMs ?? 1000));
228
+ return handshakeOnce(baseFetch, nodeUrl, transportKey);
229
+ }
230
+ }
231
+ async function handshakeOnce(baseFetch, nodeUrl, transportKey) {
208
232
  const noise = await initiate(transportKey, PROLOGUE);
209
233
  const response = await baseFetch(`${nodeUrl}${HANDSHAKE_PATH}`, {
210
234
  method: 'POST',
@@ -214,7 +238,7 @@ async function handshake(baseFetch, nodeUrl, transportKey) {
214
238
  if (response.headers.get('content-type') !== SEALED_CONTENT_TYPE) {
215
239
  throw await envelopeRefusal(response);
216
240
  }
217
- const reply = new Uint8Array(await response.arrayBuffer());
241
+ const reply = await readAtMost(response, MAX_HANDSHAKE_REPLY);
218
242
  return sessionFrom(noise, reply);
219
243
  }
220
244
  /** Finish the handshake with the node's reply. Exported for the published vectors. */
@@ -353,6 +377,10 @@ function frameReader(reader) {
353
377
  if (!(await fill(4)))
354
378
  return null;
355
379
  const length = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
380
+ if (length > MAX_FRAME_LEN) {
381
+ await reader.cancel();
382
+ throw new Error('The sealed response is malformed: a frame is larger than the node sends');
383
+ }
356
384
  if (!(await fill(4 + length)))
357
385
  return null;
358
386
  const frame = buffer.slice(4, 4 + length);
@@ -362,6 +390,23 @@ function frameReader(reader) {
362
390
  cancel: () => reader.cancel(),
363
391
  };
364
392
  }
393
+ /** Read a body, refusing one longer than `limit` rather than buffering it. */
394
+ async function readAtMost(response, limit) {
395
+ if (!response.body)
396
+ return new Uint8Array();
397
+ const reader = response.body.getReader();
398
+ let bytes = new Uint8Array();
399
+ for (;;) {
400
+ const { done, value } = await reader.read();
401
+ if (done)
402
+ return bytes;
403
+ bytes = concat(bytes, value);
404
+ if (bytes.length > limit) {
405
+ await reader.cancel();
406
+ throw new Error('The handshake reply is malformed: it is too long');
407
+ }
408
+ }
409
+ }
365
410
  async function envelopeRefusal(response) {
366
411
  let code;
367
412
  let message = `Sealed request refused: HTTP ${response.status}`;
@@ -377,5 +422,7 @@ async function envelopeRefusal(response) {
377
422
  }
378
423
  if (code === 'stale_transport_key')
379
424
  return new StaleTransportKeyError();
380
- return new SealedTransportError(response.status, code, message);
425
+ const retryAfter = Number(response.headers.get('retry-after'));
426
+ const retryAfterMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 5) * 1000 : undefined;
427
+ return new SealedTransportError(response.status, code, message, retryAfterMs);
381
428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "19.24.0",
3
+ "version": "19.24.1",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {