@calimero-network/mero-js 25.2.0 → 25.3.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.
@@ -11,7 +11,7 @@
11
11
  export interface ProofRequest {
12
12
  /** `GET`, `POST`, … exactly as it will be sent. */
13
13
  method: string;
14
- /** The URL path the node will see, **without** the query string. */
14
+ /** The path and query the node will see, as `signedPath` reads them. */
15
15
  path: string;
16
16
  /** The exact bytes that will be sent, if any. */
17
17
  body?: unknown;
@@ -18,14 +18,13 @@
18
18
  * three. The node's verifier handles both, so which you produce here is a
19
19
  * question about where your key lives, not about what the node accepts.
20
20
  *
21
- * **What it binds.** Method, path and a hash of the body. A signature for
22
- * `GET /a` is not a signature for `POST /a`, for `GET /b`, or for the same call
23
- * with different arguments.
21
+ * **What it binds.** Method, path and query, and a hash of the body. A
22
+ * signature for `GET /a?x=1` is not a signature for `POST /a?x=1`, for
23
+ * `GET /a?x=2`, for `GET /b?x=1`, or for the same call with a different body.
24
24
  *
25
- * **The path excludes the query string**, deliberately: a proxy may legitimately
26
- * rewrite it — a token parameter most of all — and signing over bytes something
27
- * else is entitled to change means failing for reasons the caller cannot see.
28
- * Anything that must be bound belongs in the body.
25
+ * **The path includes the query string**, exactly as it goes on the request
26
+ * line: no re-encoding, no reordering. A proxy between you and the node must
27
+ * forward both unchanged, or the node refuses the proof.
29
28
  *
30
29
  * **The method is not normalized.** `HEAD` and `GET` are the same *permission*
31
30
  * and the node's permission layer folds them, but they are different requests.
@@ -43,6 +42,7 @@
43
42
  * notice this file depends on both. This module's test asserts the same vectors.
44
43
  */
45
44
  import { fromHex } from "../crypto/internal.js";
45
+ export declare const SIGN_DOMAIN: Uint8Array;
46
46
  /**
47
47
  * Commit to a request body.
48
48
  *
@@ -59,8 +59,8 @@ export interface RequestSigInput {
59
59
  */
60
60
  method: string;
61
61
  /**
62
- * The path, **without** the query string. `/admin-api/namespaces`, not
63
- * `/admin-api/namespaces?token=…`.
62
+ * The path and query, exactly as on the request line:
63
+ * `/admin-api/blobs?context_id=…`. Use `signedPath` to take it from a URL.
64
64
  */
65
65
  path: string;
66
66
  /**
@@ -18,14 +18,13 @@
18
18
  * three. The node's verifier handles both, so which you produce here is a
19
19
  * question about where your key lives, not about what the node accepts.
20
20
  *
21
- * **What it binds.** Method, path and a hash of the body. A signature for
22
- * `GET /a` is not a signature for `POST /a`, for `GET /b`, or for the same call
23
- * with different arguments.
21
+ * **What it binds.** Method, path and query, and a hash of the body. A
22
+ * signature for `GET /a?x=1` is not a signature for `POST /a?x=1`, for
23
+ * `GET /a?x=2`, for `GET /b?x=1`, or for the same call with a different body.
24
24
  *
25
- * **The path excludes the query string**, deliberately: a proxy may legitimately
26
- * rewrite it — a token parameter most of all — and signing over bytes something
27
- * else is entitled to change means failing for reasons the caller cannot see.
28
- * Anything that must be bound belongs in the body.
25
+ * **The path includes the query string**, exactly as it goes on the request
26
+ * line: no re-encoding, no reordering. A proxy between you and the node must
27
+ * forward both unchanged, or the node refuses the proof.
29
28
  *
30
29
  * **The method is not normalized.** `HEAD` and `GET` are the same *permission*
31
30
  * and the node's permission layer folds them, but they are different requests.
@@ -43,7 +42,7 @@
43
42
  * notice this file depends on both. This module's test asserts the same vectors.
44
43
  */
45
44
  import { concat, domainHash, fromHex, hex, importSigningKey, u32le, u64le, } from "../crypto/internal.js";
46
- const SIGN_DOMAIN = new TextEncoder().encode("calimero.auth.request.v1");
45
+ export const SIGN_DOMAIN = new TextEncoder().encode("calimero.auth.request.v2");
47
46
  /**
48
47
  * Its own domain, distinct from the signing one.
49
48
  *
@@ -13,7 +13,7 @@ export interface ProofCredential {
13
13
  /** One request, as the proof has to see it. */
14
14
  export interface RequestToAuthorize {
15
15
  method: string;
16
- /** Path only — a query string is excluded from the signature by design. */
16
+ /** Path and query, as `signedPath` reads them from the URL being fetched. */
17
17
  path: string;
18
18
  /** The body exactly as it will be sent, if there is one. */
19
19
  body?: string | Uint8Array;
@@ -36,13 +36,6 @@ export type RequestAuthorizer = (request: RequestToAuthorize) => Promise<Record<
36
36
  * in the wrong place.
37
37
  */
38
38
  export declare function createProofAuthorizer(credential: ProofCredential): RequestAuthorizer;
39
- /**
40
- * The path a proof must be signed over, taken from a URL.
41
- *
42
- * Callers hold a base URL and a relative path; the proof commits to the path the
43
- * node will see, which is their join with the query dropped. Deriving it from
44
- * the assembled URL rather than concatenating strings is what keeps a base URL
45
- * carrying its own prefix (`…/admin-api`) from signing a path the node never
46
- * receives.
47
- */
39
+ /** The path and query a proof signs, read from the parsed URL: the serialization
40
+ * `fetch` puts on the request line, base-URL prefix and escapes included. */
48
41
  export declare function signedPath(url: string): string;
@@ -51,15 +51,10 @@ export function createProofAuthorizer(credential) {
51
51
  return { [REQUEST_PROOF_HEADER]: proof };
52
52
  };
53
53
  }
54
- /**
55
- * The path a proof must be signed over, taken from a URL.
56
- *
57
- * Callers hold a base URL and a relative path; the proof commits to the path the
58
- * node will see, which is their join with the query dropped. Deriving it from
59
- * the assembled URL rather than concatenating strings is what keeps a base URL
60
- * carrying its own prefix (`…/admin-api`) from signing a path the node never
61
- * receives.
62
- */
54
+ /** The path and query a proof signs, read from the parsed URL: the serialization
55
+ * `fetch` puts on the request line, base-URL prefix and escapes included. */
63
56
  export function signedPath(url) {
64
- return new URL(url).pathname;
57
+ // A relative URL resolves as `fetch` resolves it: against the page, if any.
58
+ const parsed = new URL(url, globalThis.location?.href ?? 'http://localhost');
59
+ return parsed.pathname + parsed.search;
65
60
  }
@@ -11,11 +11,8 @@ export interface RequestProofInput {
11
11
  */
12
12
  method: string;
13
13
  /**
14
- * The path, **without** the query string.
15
- *
16
- * Excluded because a proxy may legitimately rewrite a query, and a signature
17
- * over bytes something else may change fails for reasons the caller cannot
18
- * see. Anything that must be bound belongs in the body.
14
+ * The path and query, exactly as on the request line, with no re-encoding or
15
+ * reordering. Use `signedPath` to take it from a URL.
19
16
  */
20
17
  path: string;
21
18
  /**
@@ -48,7 +45,7 @@ export interface RequestProofInput {
48
45
  *
49
46
  * CallerProof = <account_proof bytes> || 0x00 (no session) || RequestSig
50
47
  * RequestSig = u32le(len method) || method
51
- * || u32le(len path) || path
48
+ * || u32le(len path) || path (with its query)
52
49
  * || body_hash[32]
53
50
  * || u64le(issued_at) || u64le(expires_at)
54
51
  * || signature[64]
@@ -24,14 +24,8 @@
24
24
  * this authenticates a caller without granting operator actions.
25
25
  */
26
26
  import { concat, domainHash, fromHex, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
27
+ import { requestBodyHash, SIGN_DOMAIN } from '../request/request-sig.js';
27
28
  const utf8 = (text) => new TextEncoder().encode(text);
28
- /** `calimero.auth.request.v1` — what the request signature is separated by. */
29
- const REQUEST_SIGN_DOMAIN = utf8('calimero.auth.request.v1');
30
- /**
31
- * `calimero.auth.request.body.v1` — distinct from the signing domain so a body
32
- * hash can never be presented as a request signature preimage.
33
- */
34
- const REQUEST_BODY_DOMAIN = utf8('calimero.auth.request.body.v1');
35
29
  /** The header a signed request carries. */
36
30
  export const REQUEST_PROOF_HEADER = 'X-Calimero-Proof';
37
31
  /** Seconds a signature stays honourable. Short, because it bounds replay. */
@@ -47,7 +41,7 @@ const DEFAULT_VALID_FOR = 300;
47
41
  *
48
42
  * CallerProof = <account_proof bytes> || 0x00 (no session) || RequestSig
49
43
  * RequestSig = u32le(len method) || method
50
- * || u32le(len path) || path
44
+ * || u32le(len path) || path (with its query)
51
45
  * || body_hash[32]
52
46
  * || u64le(issued_at) || u64le(expires_at)
53
47
  * || signature[64]
@@ -59,20 +53,10 @@ export async function signRequestProof(input) {
59
53
  if (!path.startsWith('/')) {
60
54
  throw new Error(`signRequestProof needs a path, got ${JSON.stringify(path)}`);
61
55
  }
62
- if (path.includes('?')) {
63
- throw new Error(`the path must carry no query string, got ${JSON.stringify(path)} — ` +
64
- 'a query is excluded from the signature on purpose, because a proxy may ' +
65
- 'rewrite one. Anything that must be bound belongs in the body.');
66
- }
67
- const bodyBytes = input.body === undefined
68
- ? new Uint8Array()
69
- : typeof input.body === 'string'
70
- ? utf8(input.body)
71
- : input.body;
72
- const bodyHash = await domainHash(REQUEST_BODY_DOMAIN, [bodyBytes]);
56
+ const bodyHash = await requestBodyHash(input.body);
73
57
  const issuedAt = Math.floor(input.now ?? Date.now() / 1000);
74
58
  const expiresAt = issuedAt + (input.validForSeconds ?? DEFAULT_VALID_FOR);
75
- const payload = await domainHash(REQUEST_SIGN_DOMAIN, [
59
+ const payload = await domainHash(SIGN_DOMAIN, [
76
60
  utf8(method),
77
61
  utf8(path),
78
62
  bodyHash,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "25.2.0",
3
+ "version": "25.3.1",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {