@calimero-network/mero-js 19.24.0 → 19.25.0

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.cjs CHANGED
@@ -929,6 +929,14 @@ var AdminApiClient = class {
929
929
  async healthCheck() {
930
930
  return unwrap(await this.httpClient.get("/admin-api/health"));
931
931
  }
932
+ /**
933
+ * Whether the node has finished starting and its store answers. Public, like
934
+ * {@link healthCheck}; unlike it, a node that is up but not ready answers
935
+ * `503`, which this surfaces as an `HTTPError` whose body names the stage.
936
+ */
937
+ async readinessCheck() {
938
+ return unwrap(await this.httpClient.get("/admin-api/ready"));
939
+ }
932
940
  async isAuthed() {
933
941
  return this.httpClient.get("/admin-api/is-authed");
934
942
  }
@@ -1323,6 +1331,23 @@ var AdminApiClient = class {
1323
1331
  )
1324
1332
  );
1325
1333
  }
1334
+ /**
1335
+ * Read a context as the session's ACCOUNT, without minting a warrant.
1336
+ *
1337
+ * Needs an account-authenticated session — one from {@link login}, not the
1338
+ * admin password login — and refuses otherwise with a `401`. The account must
1339
+ * be a member of the group owning the context (`403` if not), and the method
1340
+ * must be declared read-only in the app's ABI (`409` if not; a write needs
1341
+ * {@link performIntent}).
1342
+ */
1343
+ async queryContext(contextId, request) {
1344
+ return unwrap(
1345
+ await this.httpClient.post(
1346
+ `/admin-api/contexts/${contextId}/query`,
1347
+ request
1348
+ )
1349
+ );
1350
+ }
1326
1351
  /**
1327
1352
  * Ask what this node can do for a member in `contextId`, before they sign
1328
1353
  * anything.
@@ -1645,6 +1670,32 @@ var AdminApiClient = class {
1645
1670
  }
1646
1671
  return response;
1647
1672
  }
1673
+ /**
1674
+ * Member accounts in the group and the devices bound to each. An admin node
1675
+ * sees every account; a plain member only its own. `403` if this node is not a
1676
+ * member of the group.
1677
+ */
1678
+ async listMemberDevices(groupId, options = {}) {
1679
+ const params = new URLSearchParams();
1680
+ if (options.offset !== void 0) params.set("offset", String(options.offset));
1681
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
1682
+ const query = params.toString();
1683
+ const path = `/admin-api/groups/${groupId}/member-devices${query ? `?${query}` : ""}`;
1684
+ return this.httpClient.get(path);
1685
+ }
1686
+ /**
1687
+ * Seal a small payload to a member account's root key. This node must be a
1688
+ * member of the group, and `account` must be an account the group knows, so
1689
+ * the route reaches no further than the caller already does.
1690
+ */
1691
+ async sealToAccount(groupId, account, request) {
1692
+ return unwrap(
1693
+ await this.httpClient.post(
1694
+ `/admin-api/groups/${groupId}/accounts/${account}/seal`,
1695
+ request
1696
+ )
1697
+ );
1698
+ }
1648
1699
  async listGroupContexts(groupId) {
1649
1700
  return unwrap(await this.httpClient.get(`/admin-api/groups/${groupId}/contexts`));
1650
1701
  }
@@ -6760,6 +6811,8 @@ var HEADER_LEN = 1 + SESSION_ID_LEN + 8;
6760
6811
  var FRAME_HEAD = 0;
6761
6812
  var FRAME_DATA = 1;
6762
6813
  var FRAME_END = 2;
6814
+ var MAX_FRAME_LEN = 1 + 64 * 1024 + 16;
6815
+ var MAX_HANDSHAKE_REPLY = 1024;
6763
6816
  var RENEW_BEFORE_MS = 6e4;
6764
6817
  var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
6765
6818
  var NULL_BODY_STATUSES = [101, 204, 205, 304];
@@ -6771,10 +6824,11 @@ var StaleTransportKeyError = class extends Error {
6771
6824
  }
6772
6825
  };
6773
6826
  var SealedTransportError = class extends Error {
6774
- constructor(status, code, message) {
6827
+ constructor(status, code, message, retryAfterMs) {
6775
6828
  super(message);
6776
6829
  this.status = status;
6777
6830
  this.code = code;
6831
+ this.retryAfterMs = retryAfterMs;
6778
6832
  this.name = "SealedTransportError";
6779
6833
  }
6780
6834
  };
@@ -6878,6 +6932,15 @@ async function checkedKey(key) {
6878
6932
  return bytes;
6879
6933
  }
6880
6934
  async function handshake(baseFetch, nodeUrl, transportKey) {
6935
+ try {
6936
+ return await handshakeOnce(baseFetch, nodeUrl, transportKey);
6937
+ } catch (error) {
6938
+ if (!(error instanceof SealedTransportError) || error.code !== "busy") throw error;
6939
+ await new Promise((resolve) => setTimeout(resolve, error.retryAfterMs ?? 1e3));
6940
+ return handshakeOnce(baseFetch, nodeUrl, transportKey);
6941
+ }
6942
+ }
6943
+ async function handshakeOnce(baseFetch, nodeUrl, transportKey) {
6881
6944
  const noise = await initiate(transportKey, PROLOGUE);
6882
6945
  const response = await baseFetch(`${nodeUrl}${HANDSHAKE_PATH}`, {
6883
6946
  method: "POST",
@@ -6887,7 +6950,7 @@ async function handshake(baseFetch, nodeUrl, transportKey) {
6887
6950
  if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
6888
6951
  throw await envelopeRefusal(response);
6889
6952
  }
6890
- const reply = new Uint8Array(await response.arrayBuffer());
6953
+ const reply = await readAtMost(response, MAX_HANDSHAKE_REPLY);
6891
6954
  return sessionFrom(noise, reply);
6892
6955
  }
6893
6956
  async function sessionFrom(noise, reply) {
@@ -7013,6 +7076,10 @@ function frameReader(reader) {
7013
7076
  async next() {
7014
7077
  if (!await fill(4)) return null;
7015
7078
  const length = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
7079
+ if (length > MAX_FRAME_LEN) {
7080
+ await reader.cancel();
7081
+ throw new Error("The sealed response is malformed: a frame is larger than the node sends");
7082
+ }
7016
7083
  if (!await fill(4 + length)) return null;
7017
7084
  const frame = buffer.slice(4, 4 + length);
7018
7085
  buffer = buffer.slice(4 + length);
@@ -7021,6 +7088,20 @@ function frameReader(reader) {
7021
7088
  cancel: () => reader.cancel()
7022
7089
  };
7023
7090
  }
7091
+ async function readAtMost(response, limit) {
7092
+ if (!response.body) return new Uint8Array();
7093
+ const reader = response.body.getReader();
7094
+ let bytes = new Uint8Array();
7095
+ for (; ; ) {
7096
+ const { done, value } = await reader.read();
7097
+ if (done) return bytes;
7098
+ bytes = concat(bytes, value);
7099
+ if (bytes.length > limit) {
7100
+ await reader.cancel();
7101
+ throw new Error("The handshake reply is malformed: it is too long");
7102
+ }
7103
+ }
7104
+ }
7024
7105
  async function envelopeRefusal(response) {
7025
7106
  let code;
7026
7107
  let message = `Sealed request refused: HTTP ${response.status}`;
@@ -7031,7 +7112,9 @@ async function envelopeRefusal(response) {
7031
7112
  } catch {
7032
7113
  }
7033
7114
  if (code === "stale_transport_key") return new StaleTransportKeyError();
7034
- return new SealedTransportError(response.status, code, message);
7115
+ const retryAfter = Number(response.headers.get("retry-after"));
7116
+ const retryAfterMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 5) * 1e3 : void 0;
7117
+ return new SealedTransportError(response.status, code, message, retryAfterMs);
7035
7118
  }
7036
7119
 
7037
7120
  // src/namespace-op/namespace-op.ts