@calimero-network/mero-js 19.15.0 → 19.16.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.
@@ -11,6 +11,17 @@
11
11
  export declare function u32le(value: number): Uint8Array;
12
12
  export declare function hex(bytes: Uint8Array): string;
13
13
  export declare function fromHex(value: string, label: string, expectedBytes: number): Uint8Array;
14
+ /**
15
+ * Decode hex of an unknown length.
16
+ *
17
+ * The sized {@link fromHex} is the right default — every key and digest here
18
+ * has a fixed width, and checking it catches a truncated paste at the edge
19
+ * rather than deep inside a signature check. This one exists for the already-
20
+ * encoded structures whose length is genuinely variable: a device certificate,
21
+ * a login statement, a request signature. It still rejects odd-length and
22
+ * non-hex input, so the only check it gives up is the one that cannot apply.
23
+ */
24
+ export declare function fromHexUnsized(value: string, label: string): Uint8Array;
14
25
  export declare function u64le(value: number | bigint): Uint8Array;
15
26
  export declare function concat(...parts: Uint8Array[]): Uint8Array;
16
27
  /**
@@ -34,6 +34,27 @@ export function fromHex(value, label, expectedBytes) {
34
34
  }
35
35
  return bytes;
36
36
  }
37
+ /**
38
+ * Decode hex of an unknown length.
39
+ *
40
+ * The sized {@link fromHex} is the right default — every key and digest here
41
+ * has a fixed width, and checking it catches a truncated paste at the edge
42
+ * rather than deep inside a signature check. This one exists for the already-
43
+ * encoded structures whose length is genuinely variable: a device certificate,
44
+ * a login statement, a request signature. It still rejects odd-length and
45
+ * non-hex input, so the only check it gives up is the one that cannot apply.
46
+ */
47
+ export function fromHexUnsized(value, label) {
48
+ const clean = value.trim();
49
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
50
+ throw new Error(`${label} must be an even number of hex characters`);
51
+ }
52
+ const bytes = new Uint8Array(clean.length / 2);
53
+ for (let i = 0; i < bytes.length; i += 1) {
54
+ bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
55
+ }
56
+ return bytes;
57
+ }
37
58
  export function u64le(value) {
38
59
  const out = new Uint8Array(8);
39
60
  new DataView(out.buffer).setBigUint64(0, BigInt(value), true);
@@ -3,6 +3,7 @@ export declare function createHttpClient(transport: Transport): HttpClient;
3
3
  export declare function createBrowserHttpClient(options: {
4
4
  baseUrl: string;
5
5
  getAuthToken?: () => Promise<string | undefined>;
6
+ getProof?: Transport['getProof'];
6
7
  onTokenRefresh?: (newToken: string) => Promise<void>;
7
8
  /**
8
9
  * Callback to refresh the access token when a 401 error with 'token_expired' is detected.
@@ -25,6 +26,7 @@ export declare function createNodeHttpClient(options: {
25
26
  baseUrl: string;
26
27
  fetch?: typeof fetch;
27
28
  getAuthToken?: () => Promise<string | undefined>;
29
+ getProof?: Transport['getProof'];
28
30
  onTokenRefresh?: (newToken: string) => Promise<void>;
29
31
  /**
30
32
  * Callback to refresh the access token when a 401 error with 'token_expired' is detected.
@@ -47,6 +49,7 @@ export declare function createUniversalHttpClient(options: {
47
49
  baseUrl: string;
48
50
  fetch?: typeof fetch;
49
51
  getAuthToken?: () => Promise<string | undefined>;
52
+ getProof?: Transport['getProof'];
50
53
  onTokenRefresh?: (newToken: string) => Promise<void>;
51
54
  /**
52
55
  * Callback to refresh the access token when a 401 error with 'token_expired' is detected.
@@ -11,6 +11,7 @@ export function createBrowserHttpClient(options) {
11
11
  fetch: (url, init) => globalThis.fetch(url, init),
12
12
  baseUrl: options.baseUrl,
13
13
  getAuthToken: options.getAuthToken,
14
+ getProof: options.getProof,
14
15
  onTokenRefresh: options.onTokenRefresh,
15
16
  refreshToken: options.refreshToken,
16
17
  onAuthRevoked: options.onAuthRevoked,
@@ -42,6 +43,7 @@ export function createNodeHttpClient(options) {
42
43
  : (url, init) => fetchImpl(url, init),
43
44
  baseUrl: options.baseUrl,
44
45
  getAuthToken: options.getAuthToken,
46
+ getProof: options.getProof,
45
47
  onTokenRefresh: options.onTokenRefresh,
46
48
  refreshToken: options.refreshToken,
47
49
  onAuthRevoked: options.onAuthRevoked,
@@ -4,6 +4,21 @@ export interface Transport {
4
4
  baseUrl: string;
5
5
  defaultHeaders?: Record<string, string>;
6
6
  getAuthToken?: () => Promise<string | undefined>;
7
+ /**
8
+ * Sign each request instead of, or alongside, presenting a session.
9
+ *
10
+ * Called per call with the method, the path the node will see, and the exact
11
+ * body — a proof commits to all three, so unlike `getAuthToken` it cannot be
12
+ * resolved once and reused. Returning `undefined` sends no proof header,
13
+ * which is how a client that only sometimes has a key stays usable.
14
+ *
15
+ * `createProofSigner` builds one of these from a credential and a key.
16
+ */
17
+ getProof?: (request: {
18
+ method: string;
19
+ path: string;
20
+ body?: unknown;
21
+ }) => Promise<string | undefined>;
7
22
  onTokenRefresh?: (newToken: string) => Promise<void>;
8
23
  /**
9
24
  * Callback to refresh the access token when a 401 error with 'token_expired' is detected.
@@ -88,6 +88,23 @@ function headersToRecord(headers) {
88
88
  return record;
89
89
  }
90
90
  // Web Standards HTTP client implementation
91
+ /**
92
+ * The path a node will verify a request signature against.
93
+ *
94
+ * `new URL` drops the query and keeps any prefix the base URL contributed,
95
+ * which is exactly what reaches the server. The fallback covers a `url` that is
96
+ * already relative — it cannot be parsed, and signing the whole string with its
97
+ * query attached would be worse than signing the part before the `?`.
98
+ */
99
+ function pathForProof(url) {
100
+ try {
101
+ return new URL(url).pathname;
102
+ }
103
+ catch {
104
+ const query = url.indexOf('?');
105
+ return query === -1 ? url : url.slice(0, query);
106
+ }
107
+ }
91
108
  export class WebHttpClient {
92
109
  constructor(transport) {
93
110
  this.transport = transport;
@@ -234,6 +251,26 @@ export class WebHttpClient {
234
251
  if (init?.keepalive !== undefined) {
235
252
  requestInit.keepalive = init.keepalive;
236
253
  }
254
+ // Signed here rather than in `buildHeaders`, which sees neither the method
255
+ // nor the body — and a proof commits to both. Signed AFTER the body is
256
+ // settled above, so what is hashed is what is sent.
257
+ //
258
+ // The path comes from the built URL's `pathname`, not from the `path`
259
+ // argument: that argument may carry a query string, and may be relative to
260
+ // a base URL that contributes its own prefix. The node verifies against the
261
+ // path it received, so the only safe source is the URL actually being
262
+ // fetched. A query string is deliberately not covered — core's `RequestSig`
263
+ // signs the path alone, and signing more here would fail every request.
264
+ if (this.transport.getProof) {
265
+ const proof = await this.transport.getProof({
266
+ method: requestInit.method ?? 'GET',
267
+ path: pathForProof(url),
268
+ body: requestInit.body,
269
+ });
270
+ if (proof) {
271
+ headersObj['X-Calimero-Proof'] = proof;
272
+ }
273
+ }
237
274
  try {
238
275
  const response = await this.transport.fetch(url, requestInit);
239
276
  if (!response.ok) {