@calimero-network/mero-js 19.15.0 → 19.17.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/crypto/internal.d.ts +11 -0
- package/dist/crypto/internal.js +21 -0
- package/dist/http-client/http-factory.d.ts +3 -0
- package/dist/http-client/http-factory.js +2 -0
- package/dist/http-client/http-types.d.ts +15 -0
- package/dist/http-client/web-client.js +37 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +223 -143
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +223 -143
- package/dist/index.mjs.map +3 -3
- package/dist/mero-js.d.ts +35 -0
- package/dist/mero-js.js +18 -0
- package/dist/request/caller-proof.d.ts +43 -0
- package/dist/request/caller-proof.js +36 -0
- package/dist/request/index.d.ts +2 -0
- package/dist/request/index.js +2 -0
- package/dist/request/proof-signer.d.ts +49 -0
- package/dist/request/proof-signer.js +59 -0
- package/package.json +1 -1
package/dist/mero-js.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AuthApiClient } from './auth-api/index.js';
|
|
2
2
|
import type { AdminApiClient } from './admin-api/index.js';
|
|
3
3
|
import type { TokenStore } from './token-store/index.js';
|
|
4
|
+
import type { ProofRequest, ProofSignerOptions } from './request/index.js';
|
|
4
5
|
import type { AuthCallbackResult, AuthLoginOptions } from './auth/index.js';
|
|
5
6
|
import { RpcClient } from './rpc/index.js';
|
|
6
7
|
import { SseClient } from './events/sse.js';
|
|
@@ -44,6 +45,40 @@ export interface MeroJsConfig {
|
|
|
44
45
|
* instead — it returns a connection that can write without one.
|
|
45
46
|
*/
|
|
46
47
|
cloud?: CloudClientConfig;
|
|
48
|
+
/**
|
|
49
|
+
* Sign every request with a device key, instead of or alongside a session.
|
|
50
|
+
*
|
|
51
|
+
* A session is a bearer token: obtained once, presented many times. A
|
|
52
|
+
* request-carried proof commits to one method, one path and one body, so it
|
|
53
|
+
* is minted per call and cannot be lifted onto another request. It is what
|
|
54
|
+
* lets a client talk to a relay it has no account on — nothing was ever
|
|
55
|
+
* issued to it.
|
|
56
|
+
*
|
|
57
|
+
* Pass the keys and let the SDK mint each proof:
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* new MeroJs({
|
|
61
|
+
* baseUrl,
|
|
62
|
+
* requestProof: { credential, session, signerSecret },
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* …or pass your own signer, when the key must not reach the SDK at all — a
|
|
67
|
+
* hardware token, or a worker that holds it:
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* new MeroJs({ baseUrl, requestProof: (req) => myWorker.sign(req) });
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* The node only honours this when it runs with `--delegated-access`; a node
|
|
74
|
+
* that does not answers `403`, which is distinct from the `401` a bad proof
|
|
75
|
+
* gets.
|
|
76
|
+
*
|
|
77
|
+
* Applies to the request/response API. `events` (SSE) and `ws` are long-lived
|
|
78
|
+
* connections rather than requests, so a per-request signature does not
|
|
79
|
+
* describe them — they still use the session.
|
|
80
|
+
*/
|
|
81
|
+
requestProof?: ProofSignerOptions | ((request: ProofRequest) => Promise<string | undefined>);
|
|
47
82
|
}
|
|
48
83
|
export interface TokenData {
|
|
49
84
|
access_token: string;
|
package/dist/mero-js.js
CHANGED
|
@@ -2,11 +2,24 @@ import { createBrowserHttpClient } from './http-client/index.js';
|
|
|
2
2
|
import { createAuthApiClientFromHttpClient } from './auth-api/index.js';
|
|
3
3
|
import { createAdminApiClientFromHttpClient } from './admin-api/index.js';
|
|
4
4
|
import { parseAuthCallback, buildAuthLoginUrl } from './auth/index.js';
|
|
5
|
+
import { createProofSigner } from './request/index.js';
|
|
5
6
|
import { RpcClient } from './rpc/index.js';
|
|
6
7
|
import { SseClient } from './events/sse.js';
|
|
7
8
|
import { WsClient } from './events/ws.js';
|
|
8
9
|
import { EphemeralClient } from './ephemeral/index.js';
|
|
9
10
|
import { CloudClient } from './cloud/cloud-client.js';
|
|
11
|
+
/**
|
|
12
|
+
* Accept either the keys or a ready signer.
|
|
13
|
+
*
|
|
14
|
+
* Taking a callback as well as an options object is not a convenience: it is
|
|
15
|
+
* how a caller keeps the signing key out of the SDK entirely when it lives
|
|
16
|
+
* somewhere the SDK cannot reach.
|
|
17
|
+
*/
|
|
18
|
+
function resolveProofSigner(config) {
|
|
19
|
+
if (!config)
|
|
20
|
+
return undefined;
|
|
21
|
+
return typeof config === 'function' ? config : createProofSigner(config);
|
|
22
|
+
}
|
|
10
23
|
/** Name of the Web Lock that serializes token refresh across tabs / webviews. */
|
|
11
24
|
const REFRESH_LOCK_NAME = 'mero-js:token-refresh';
|
|
12
25
|
/** Returns `navigator.locks` when the runtime supports the Web Locks API. */
|
|
@@ -58,8 +71,13 @@ export class MeroJs {
|
|
|
58
71
|
// Create HTTP client with token management
|
|
59
72
|
// For Tauri, explicitly set credentials to 'omit' to avoid network errors
|
|
60
73
|
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
|
|
74
|
+
// One signer for the whole connection: `authClient` and `adminClient` are
|
|
75
|
+
// both built FROM this transport, so wiring it here covers every request
|
|
76
|
+
// either of them makes.
|
|
77
|
+
const getProof = resolveProofSigner(this.config.requestProof);
|
|
61
78
|
this.httpClient = createBrowserHttpClient({
|
|
62
79
|
baseUrl: this.config.baseUrl,
|
|
80
|
+
getProof,
|
|
63
81
|
getAuthToken: async () => {
|
|
64
82
|
const token = await this.getValidToken();
|
|
65
83
|
return token?.access_token || '';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble the credential a node reads from `X-Calimero-Proof`.
|
|
3
|
+
*
|
|
4
|
+
* A request-carried proof is three links, and the node needs all of them in one
|
|
5
|
+
* header: the account root certified the device, the device (optionally)
|
|
6
|
+
* certified a short-lived session key, and that key signed THIS request. Each
|
|
7
|
+
* link is minted by its own function here — {@link signDeviceCert},
|
|
8
|
+
* {@link signLoginStatement}, {@link signRequest} — and each already returns
|
|
9
|
+
* the borsh encoding of the Rust type the node deserializes. Rust's
|
|
10
|
+
* `CallerProof` is a plain struct of those three fields, and borsh writes a
|
|
11
|
+
* struct as its fields back to back, so assembling the header is concatenation
|
|
12
|
+
* rather than a fourth encoder.
|
|
13
|
+
*
|
|
14
|
+
* That is worth stating because it is load-bearing and it is not obvious: if it
|
|
15
|
+
* were re-encoded here there would be a second spelling of a signed structure,
|
|
16
|
+
* able to disagree with the one that was signed. `caller-proof.test.ts` pins the
|
|
17
|
+
* concatenation against bytes produced by core itself.
|
|
18
|
+
*/
|
|
19
|
+
/** The three links, each already hex-encoded by the function that signed it. */
|
|
20
|
+
export interface CallerProofInput {
|
|
21
|
+
/**
|
|
22
|
+
* `signDeviceCert(...)` — the account root's certificate over the device key,
|
|
23
|
+
* with the genesis that names the account.
|
|
24
|
+
*/
|
|
25
|
+
credential: string;
|
|
26
|
+
/**
|
|
27
|
+
* `signLoginStatement(...)`, when signing with a session key.
|
|
28
|
+
*
|
|
29
|
+
* Omit it on the two-link chain, where the device key signs the request
|
|
30
|
+
* itself. Omitting is not the same as passing an empty string: borsh writes
|
|
31
|
+
* `Option` as a tag byte, so the two produce different bytes and only one of
|
|
32
|
+
* them parses.
|
|
33
|
+
*/
|
|
34
|
+
session?: string;
|
|
35
|
+
/** `signRequest(...)` — the signature over this exact method, path and body. */
|
|
36
|
+
request: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Concatenate the three links into the header value.
|
|
40
|
+
*
|
|
41
|
+
* Returns hex, ready to present as `X-Calimero-Proof`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function callerProof(input: CallerProofInput): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble the credential a node reads from `X-Calimero-Proof`.
|
|
3
|
+
*
|
|
4
|
+
* A request-carried proof is three links, and the node needs all of them in one
|
|
5
|
+
* header: the account root certified the device, the device (optionally)
|
|
6
|
+
* certified a short-lived session key, and that key signed THIS request. Each
|
|
7
|
+
* link is minted by its own function here — {@link signDeviceCert},
|
|
8
|
+
* {@link signLoginStatement}, {@link signRequest} — and each already returns
|
|
9
|
+
* the borsh encoding of the Rust type the node deserializes. Rust's
|
|
10
|
+
* `CallerProof` is a plain struct of those three fields, and borsh writes a
|
|
11
|
+
* struct as its fields back to back, so assembling the header is concatenation
|
|
12
|
+
* rather than a fourth encoder.
|
|
13
|
+
*
|
|
14
|
+
* That is worth stating because it is load-bearing and it is not obvious: if it
|
|
15
|
+
* were re-encoded here there would be a second spelling of a signed structure,
|
|
16
|
+
* able to disagree with the one that was signed. `caller-proof.test.ts` pins the
|
|
17
|
+
* concatenation against bytes produced by core itself.
|
|
18
|
+
*/
|
|
19
|
+
import { fromHexUnsized, hex, concat } from "../crypto/internal.js";
|
|
20
|
+
/**
|
|
21
|
+
* Concatenate the three links into the header value.
|
|
22
|
+
*
|
|
23
|
+
* Returns hex, ready to present as `X-Calimero-Proof`.
|
|
24
|
+
*/
|
|
25
|
+
export function callerProof(input) {
|
|
26
|
+
const credential = fromHexUnsized(input.credential, "credential");
|
|
27
|
+
const request = fromHexUnsized(input.request, "request signature");
|
|
28
|
+
// Borsh `Option<T>`: a single tag byte, then the value when present. A
|
|
29
|
+
// two-link chain is `None`, not an empty `Some` — the node reads the tag
|
|
30
|
+
// before anything else, so getting it backwards fails to parse rather than
|
|
31
|
+
// failing to verify, which at least is loud.
|
|
32
|
+
const session = input.session === undefined
|
|
33
|
+
? new Uint8Array([0])
|
|
34
|
+
: concat(new Uint8Array([1]), fromHexUnsized(input.session, "session statement"));
|
|
35
|
+
return hex(concat(credential, session, request));
|
|
36
|
+
}
|
package/dist/request/index.d.ts
CHANGED
package/dist/request/index.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a device key into something the HTTP client can call per request.
|
|
3
|
+
*
|
|
4
|
+
* A session is a bearer token: mint once, present many times. A request-carried
|
|
5
|
+
* proof is the opposite — it commits to one method, one path and one body, so
|
|
6
|
+
* it has to be produced at the moment of the call, by something holding the
|
|
7
|
+
* signing key. This is that something, shaped like `getAuthToken` so the key
|
|
8
|
+
* stays with the caller and never reaches the transport.
|
|
9
|
+
*/
|
|
10
|
+
/** What the transport knows about the call it is about to make. */
|
|
11
|
+
export interface ProofRequest {
|
|
12
|
+
/** `GET`, `POST`, … exactly as it will be sent. */
|
|
13
|
+
method: string;
|
|
14
|
+
/** The URL path the node will see, **without** the query string. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** The exact bytes that will be sent, if any. */
|
|
17
|
+
body?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface ProofSignerOptions {
|
|
20
|
+
/** `signDeviceCert(...)`, hex. */
|
|
21
|
+
credential: string;
|
|
22
|
+
/**
|
|
23
|
+
* `signLoginStatement(...)`, hex — the three-link chain.
|
|
24
|
+
*
|
|
25
|
+
* Omit for the two-link chain, where `signerSecret` is the device key itself.
|
|
26
|
+
*/
|
|
27
|
+
session?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The ed25519 secret that signs each request, hex: the session key when
|
|
30
|
+
* `session` is set, otherwise the device key.
|
|
31
|
+
*/
|
|
32
|
+
signerSecret: string;
|
|
33
|
+
/**
|
|
34
|
+
* How long each signature stays valid. Seconds, default 120.
|
|
35
|
+
*
|
|
36
|
+
* This is what bounds replay and nothing else does, so it is deliberately
|
|
37
|
+
* short and deliberately not configurable to "never". A clock skew allowance
|
|
38
|
+
* lives on the node; this only has to outlive the flight time.
|
|
39
|
+
*/
|
|
40
|
+
ttlSeconds?: number;
|
|
41
|
+
/** Unix seconds. Injectable so a test does not depend on the wall clock. */
|
|
42
|
+
now?: () => number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build the `X-Calimero-Proof` value for one request.
|
|
46
|
+
*
|
|
47
|
+
* Pass the result to the HTTP client's `getProof`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createProofSigner(options: ProofSignerOptions): (request: ProofRequest) => Promise<string>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a device key into something the HTTP client can call per request.
|
|
3
|
+
*
|
|
4
|
+
* A session is a bearer token: mint once, present many times. A request-carried
|
|
5
|
+
* proof is the opposite — it commits to one method, one path and one body, so
|
|
6
|
+
* it has to be produced at the moment of the call, by something holding the
|
|
7
|
+
* signing key. This is that something, shaped like `getAuthToken` so the key
|
|
8
|
+
* stays with the caller and never reaches the transport.
|
|
9
|
+
*/
|
|
10
|
+
import { callerProof } from "./caller-proof.js";
|
|
11
|
+
import { signRequest } from "./request-sig.js";
|
|
12
|
+
/**
|
|
13
|
+
* The bytes a body will actually be sent as.
|
|
14
|
+
*
|
|
15
|
+
* Refuses what it cannot hash without consuming or re-encoding. A stream can be
|
|
16
|
+
* read once, and reading it here would send an empty body; `FormData` and
|
|
17
|
+
* `URLSearchParams` are serialized by `fetch` in ways this cannot reproduce
|
|
18
|
+
* byte for byte. Signing a guess would produce a signature over bytes that
|
|
19
|
+
* never travelled, and the node would reject it with a signature error that
|
|
20
|
+
* says nothing about the real cause — so this throws, naming the type.
|
|
21
|
+
*/
|
|
22
|
+
function bodyBytes(body) {
|
|
23
|
+
if (body === undefined || body === null)
|
|
24
|
+
return new Uint8Array(0);
|
|
25
|
+
if (typeof body === "string")
|
|
26
|
+
return body;
|
|
27
|
+
if (body instanceof Uint8Array)
|
|
28
|
+
return body;
|
|
29
|
+
if (body instanceof ArrayBuffer)
|
|
30
|
+
return new Uint8Array(body);
|
|
31
|
+
if (ArrayBuffer.isView(body)) {
|
|
32
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`a request-carried proof cannot commit to a ${body?.constructor?.name ?? typeof body} body: pass a string or bytes, or use a session for this call`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the `X-Calimero-Proof` value for one request.
|
|
38
|
+
*
|
|
39
|
+
* Pass the result to the HTTP client's `getProof`.
|
|
40
|
+
*/
|
|
41
|
+
export function createProofSigner(options) {
|
|
42
|
+
const ttl = options.ttlSeconds ?? 120;
|
|
43
|
+
const clock = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
44
|
+
return async (request) => {
|
|
45
|
+
const issuedAt = clock();
|
|
46
|
+
return callerProof({
|
|
47
|
+
credential: options.credential,
|
|
48
|
+
session: options.session,
|
|
49
|
+
request: await signRequest({
|
|
50
|
+
method: request.method,
|
|
51
|
+
path: request.path,
|
|
52
|
+
body: bodyBytes(request.body),
|
|
53
|
+
issuedAt,
|
|
54
|
+
expiresAt: issuedAt + ttl,
|
|
55
|
+
signerSecret: options.signerSecret,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
}
|