@calimero-network/mero-js 22.7.0 → 22.8.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/account/account.d.ts +99 -4
- package/dist/account/account.js +0 -0
- package/dist/account/index.d.ts +6 -2
- package/dist/account/index.js +3 -1
- package/dist/admin-api/admin-client.d.ts +138 -2
- package/dist/admin-api/admin-client.js +237 -1
- package/dist/admin-api/admin-types.d.ts +159 -5
- package/dist/cloud/cloud-client.d.ts +38 -8
- package/dist/cloud/cloud-client.js +38 -21
- package/dist/cloud/connect.d.ts +101 -2
- package/dist/cloud/connect.js +84 -8
- package/dist/cloud/enrol-redirect.d.ts +195 -0
- package/dist/cloud/enrol-redirect.js +236 -0
- package/dist/cloud/index.d.ts +6 -2
- package/dist/cloud/index.js +7 -1
- package/dist/cloud/link-redirect.d.ts +85 -0
- package/dist/cloud/link-redirect.js +105 -0
- package/dist/crypto/internal.d.ts +15 -0
- package/dist/crypto/internal.js +18 -0
- package/dist/device-cert/device-cert.d.ts +85 -2
- package/dist/device-cert/device-cert.js +100 -9
- package/dist/device-cert/index.d.ts +2 -2
- package/dist/device-cert/index.js +1 -1
- package/dist/events/sse.d.ts +22 -2
- package/dist/events/sse.js +20 -11
- package/dist/http-client/http-types.d.ts +18 -0
- package/dist/http-client/web-client.js +21 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +1666 -365
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +16 -2
- package/dist/index.mjs +1666 -365
- package/dist/index.mjs.map +4 -4
- package/dist/login/login.d.ts +11 -1
- package/dist/login/login.js +5 -4
- package/dist/login/session.d.ts +21 -5
- package/dist/login/session.js +7 -15
- package/dist/namespace-op/namespace-op.d.ts +14 -2
- package/dist/namespace-op/namespace-op.js +9 -5
- package/dist/relay/index.d.ts +2 -2
- package/dist/relay/index.js +1 -1
- package/dist/relay/nonce-source.d.ts +135 -0
- package/dist/relay/nonce-source.js +187 -19
- package/dist/relay/relay-client.d.ts +60 -5
- package/dist/relay/relay-client.js +84 -4
- package/dist/relay-attestation/attest-node-key.d.ts +66 -0
- package/dist/relay-attestation/attest-node-key.js +110 -0
- package/dist/relay-attestation/index.d.ts +1 -0
- package/dist/relay-attestation/index.js +1 -0
- package/dist/request-proof/authorizer.d.ts +48 -0
- package/dist/request-proof/authorizer.js +65 -0
- package/dist/request-proof/index.d.ts +4 -0
- package/dist/request-proof/index.js +2 -0
- package/dist/request-proof/request-proof.d.ts +56 -0
- package/dist/request-proof/request-proof.js +111 -0
- package/dist/rpc/index.d.ts +21 -0
- package/dist/rpc/index.js +20 -0
- package/dist/transport/index.d.ts +6 -0
- package/dist/transport/index.js +3 -0
- package/dist/transport/mero-client.d.ts +286 -0
- package/dist/transport/mero-client.js +379 -0
- package/dist/transport/relay-observer.d.ts +152 -0
- package/dist/transport/relay-observer.js +173 -0
- package/dist/transport/relay-transport.d.ts +73 -0
- package/dist/transport/relay-transport.js +94 -0
- package/dist/transport/types.d.ts +100 -0
- package/dist/transport/types.js +32 -0
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing one request, so a caller's identity travels with it.
|
|
3
|
+
*
|
|
4
|
+
* This is the credential a keyholder presents when it has no session: the device
|
|
5
|
+
* key signs the request itself, and the certificate its account signed says whose
|
|
6
|
+
* key that is. The node verifies both with no store read and no prior
|
|
7
|
+
* relationship — `crates/server/src/proof_auth.rs` — which is what lets a hosted
|
|
8
|
+
* relay serve a caller it has never seen and holds no auth provider for.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this exists alongside `login()`
|
|
11
|
+
*
|
|
12
|
+
* A session is a second link in the same chain, not an alternative to it. It
|
|
13
|
+
* earns its place where the device key sits behind a boundary worth crossing
|
|
14
|
+
* once — a hardware key, an extension — so a per-call signature becomes a
|
|
15
|
+
* per-session one. A CLI, a server job, or a browser holding its device secret
|
|
16
|
+
* in memory gains nothing from it, and `login()` additionally needs the node's
|
|
17
|
+
* signing key learned out of band. This path needs neither.
|
|
18
|
+
*
|
|
19
|
+
* Measured against a node built from core `aecba573b`: a two-link proof is
|
|
20
|
+
* served `200` on `/admin-api/namespaces`, `/contexts`, `/contexts/{id}`,
|
|
21
|
+
* `/groups/{id}`, `/groups/{id}/contexts`, `/contexts/{id}/identities` and
|
|
22
|
+
* `/storage`, and the listings are **filtered** — an account in one of a node's
|
|
23
|
+
* two namespaces is shown one. `POST /admin-api/namespaces` is refused `403`, so
|
|
24
|
+
* this authenticates a caller without granting operator actions.
|
|
25
|
+
*/
|
|
26
|
+
import { concat, domainHash, fromHex, hex, importSigningKey, u32le, u64le, } from '../crypto/internal.js';
|
|
27
|
+
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
|
+
/** The header a signed request carries. */
|
|
36
|
+
export const REQUEST_PROOF_HEADER = 'X-Calimero-Proof';
|
|
37
|
+
/** Seconds a signature stays honourable. Short, because it bounds replay. */
|
|
38
|
+
const DEFAULT_VALID_FOR = 300;
|
|
39
|
+
/**
|
|
40
|
+
* Sign one request and return the `X-Calimero-Proof` value, hex.
|
|
41
|
+
*
|
|
42
|
+
* The encoding is borsh, and it is assembled rather than re-derived: the
|
|
43
|
+
* account proof is the **first** field of `CallerProof`, so the credential hex
|
|
44
|
+
* a caller already holds is exactly its own prefix. Concatenating avoids a
|
|
45
|
+
* second implementation of a structure the node deserializes — the class of bug
|
|
46
|
+
* that surfaces far away, as a node refusing a credential nothing can explain.
|
|
47
|
+
*
|
|
48
|
+
* CallerProof = <account_proof bytes> || 0x00 (no session) || RequestSig
|
|
49
|
+
* RequestSig = u32le(len method) || method
|
|
50
|
+
* || u32le(len path) || path
|
|
51
|
+
* || body_hash[32]
|
|
52
|
+
* || u64le(issued_at) || u64le(expires_at)
|
|
53
|
+
* || signature[64]
|
|
54
|
+
*/
|
|
55
|
+
export async function signRequestProof(input) {
|
|
56
|
+
const { method, path, credential } = input;
|
|
57
|
+
if (!method)
|
|
58
|
+
throw new Error('signRequestProof needs the method the request will use');
|
|
59
|
+
if (!path.startsWith('/')) {
|
|
60
|
+
throw new Error(`signRequestProof needs a path, got ${JSON.stringify(path)}`);
|
|
61
|
+
}
|
|
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]);
|
|
73
|
+
const issuedAt = Math.floor(input.now ?? Date.now() / 1000);
|
|
74
|
+
const expiresAt = issuedAt + (input.validForSeconds ?? DEFAULT_VALID_FOR);
|
|
75
|
+
const payload = await domainHash(REQUEST_SIGN_DOMAIN, [
|
|
76
|
+
utf8(method),
|
|
77
|
+
utf8(path),
|
|
78
|
+
bodyHash,
|
|
79
|
+
u64le(issuedAt),
|
|
80
|
+
u64le(expiresAt),
|
|
81
|
+
]);
|
|
82
|
+
const signature = await signPayload(payload, input);
|
|
83
|
+
if (signature.length !== 64) {
|
|
84
|
+
throw new Error(`the signer returned ${signature.length} bytes, not a 64-byte signature`);
|
|
85
|
+
}
|
|
86
|
+
const requestSig = concat(u32le(utf8(method).length), utf8(method), u32le(utf8(path).length), utf8(path), bodyHash, u64le(issuedAt), u64le(expiresAt), signature);
|
|
87
|
+
// The credential IS the account-proof prefix. Length is not asserted: it varies
|
|
88
|
+
// with the handoff chain, and a wrong one fails as a refused proof rather than
|
|
89
|
+
// silently as something else.
|
|
90
|
+
const accountProof = hexToBytes(credential, 'credential');
|
|
91
|
+
// 0x00: borsh's `None`. The device key signed the request, so there is no
|
|
92
|
+
// session link — a different encoding, not a shorter one.
|
|
93
|
+
return hex(concat(accountProof, new Uint8Array([0]), requestSig));
|
|
94
|
+
}
|
|
95
|
+
async function signPayload(payload, input) {
|
|
96
|
+
if (input.signer)
|
|
97
|
+
return input.signer.sign(payload);
|
|
98
|
+
if (!input.deviceSecret) {
|
|
99
|
+
throw new Error('signRequestProof needs either `deviceSecret` or `signer`');
|
|
100
|
+
}
|
|
101
|
+
const key = await importSigningKey(input.deviceSecret);
|
|
102
|
+
return new Uint8Array(await crypto.subtle.sign({ name: 'Ed25519' }, key, payload));
|
|
103
|
+
}
|
|
104
|
+
/** Hex of unknown length — `fromHex` requires a fixed byte count. */
|
|
105
|
+
function hexToBytes(value, label) {
|
|
106
|
+
const clean = value.trim();
|
|
107
|
+
if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
|
|
108
|
+
throw new Error(`${label} is not an even-length hex string`);
|
|
109
|
+
}
|
|
110
|
+
return fromHex(clean, label, clean.length / 2);
|
|
111
|
+
}
|
package/dist/rpc/index.d.ts
CHANGED
|
@@ -27,10 +27,31 @@ export declare class RpcError extends Error {
|
|
|
27
27
|
export declare function jsonRpcCall<T>(httpClient: HttpClient, method: string, params: unknown): Promise<T>;
|
|
28
28
|
export declare class RpcClient {
|
|
29
29
|
private httpClient;
|
|
30
|
+
/**
|
|
31
|
+
* Which write path this is. Present so `RpcClient` satisfies
|
|
32
|
+
* `ExecuteTransport` as-is — the transport abstraction adopted *this* class's
|
|
33
|
+
* `execute` shape as the canonical one, so the node side is the interface
|
|
34
|
+
* rather than an adapter onto it.
|
|
35
|
+
*/
|
|
36
|
+
readonly kind: "node";
|
|
37
|
+
/** A node serves `/sse` and `/ws` to the credential it issued. */
|
|
38
|
+
readonly canSubscribe = true;
|
|
30
39
|
constructor(opts: {
|
|
31
40
|
httpClient: HttpClient;
|
|
32
41
|
});
|
|
33
42
|
execute<T = unknown>(params: ExecuteParams): Promise<T>;
|
|
43
|
+
/**
|
|
44
|
+
* The same call, in the shape the transport abstraction shares with the
|
|
45
|
+
* relay.
|
|
46
|
+
*
|
|
47
|
+
* `rootHash` is left absent rather than synthesized: JSON-RPC `execute` does
|
|
48
|
+
* not report the context's scope root, and inventing a value — or a `''` —
|
|
49
|
+
* would make "the node does not tell us" indistinguishable from an answer.
|
|
50
|
+
*/
|
|
51
|
+
executeWithMetadata<T = unknown>(params: ExecuteParams): Promise<{
|
|
52
|
+
returns: T;
|
|
53
|
+
transport: 'node';
|
|
54
|
+
}>;
|
|
34
55
|
/**
|
|
35
56
|
* One-tap owner-driven convert: re-signs the caller's identity-gated entries
|
|
36
57
|
* to the current schema. The export converts all of the caller's
|
package/dist/rpc/index.js
CHANGED
|
@@ -29,6 +29,15 @@ export async function jsonRpcCall(httpClient, method, params) {
|
|
|
29
29
|
}
|
|
30
30
|
export class RpcClient {
|
|
31
31
|
constructor(opts) {
|
|
32
|
+
/**
|
|
33
|
+
* Which write path this is. Present so `RpcClient` satisfies
|
|
34
|
+
* `ExecuteTransport` as-is — the transport abstraction adopted *this* class's
|
|
35
|
+
* `execute` shape as the canonical one, so the node side is the interface
|
|
36
|
+
* rather than an adapter onto it.
|
|
37
|
+
*/
|
|
38
|
+
this.kind = 'node';
|
|
39
|
+
/** A node serves `/sse` and `/ws` to the credential it issued. */
|
|
40
|
+
this.canSubscribe = true;
|
|
32
41
|
this.httpClient = opts.httpClient;
|
|
33
42
|
}
|
|
34
43
|
async execute(params) {
|
|
@@ -42,6 +51,17 @@ export class RpcClient {
|
|
|
42
51
|
}
|
|
43
52
|
return result;
|
|
44
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The same call, in the shape the transport abstraction shares with the
|
|
56
|
+
* relay.
|
|
57
|
+
*
|
|
58
|
+
* `rootHash` is left absent rather than synthesized: JSON-RPC `execute` does
|
|
59
|
+
* not report the context's scope root, and inventing a value — or a `''` —
|
|
60
|
+
* would make "the node does not tell us" indistinguishable from an answer.
|
|
61
|
+
*/
|
|
62
|
+
async executeWithMetadata(params) {
|
|
63
|
+
return { returns: await this.execute(params), transport: 'node' };
|
|
64
|
+
}
|
|
45
65
|
/**
|
|
46
66
|
* One-tap owner-driven convert: re-signs the caller's identity-gated entries
|
|
47
67
|
* to the current schema. The export converts all of the caller's
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { MeroClient, createMeroClient } from './mero-client.js';
|
|
2
|
+
export type { MeroClientConfig, NodeTransportConfig, RelayTransportConfig, } from './mero-client.js';
|
|
3
|
+
export { RelayTransport } from './relay-transport.js';
|
|
4
|
+
export { RelayObserver, defaultAudience } from './relay-observer.js';
|
|
5
|
+
export type { RelayObserveConfig } from './relay-observer.js';
|
|
6
|
+
export type { ExecuteTransport, ExecuteResult, TransportKind } from './types.js';
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One client, two transports — chosen once, at construction.
|
|
3
|
+
*
|
|
4
|
+
* # The problem this exists for
|
|
5
|
+
*
|
|
6
|
+
* An app that wants to move from "the user has a node" to "the user has a key
|
|
7
|
+
* and a relay" had to rewrite itself, because the two write paths are different
|
|
8
|
+
* objects with different method shapes. That makes the transport a property of
|
|
9
|
+
* the *application source*, when it is really a property of the connection: the
|
|
10
|
+
* same screen, the same hook, the same `execute` call wants to work either way.
|
|
11
|
+
*
|
|
12
|
+
* `createMeroClient` takes the decision at construction and nowhere else. After
|
|
13
|
+
* that, `client.rpc.execute({ contextId, method, argsJson })` is the same call
|
|
14
|
+
* against a node and against a relay — same arguments, same return type. The
|
|
15
|
+
* node shape is the one both speak, because shipped apps already write it and
|
|
16
|
+
* the alternative would be a breaking change wearing an abstraction's clothes.
|
|
17
|
+
*
|
|
18
|
+
* # Defaulting to the node is a compatibility guarantee
|
|
19
|
+
*
|
|
20
|
+
* `createMeroClient({ baseUrl })` is `createMeroJs({ baseUrl })` with a facade
|
|
21
|
+
* in front. No `transport` field means `'node'`, so nothing an existing app
|
|
22
|
+
* does can accidentally select the relay.
|
|
23
|
+
*
|
|
24
|
+
* # Events work on both transports, given one input
|
|
25
|
+
*
|
|
26
|
+
* A relay is an ordinary node, so it serves `/sse` and `/ws` to a credential it
|
|
27
|
+
* issued, and a keyholder can obtain one by logging in with its account proof.
|
|
28
|
+
* `client.events` and `client.ws` are therefore the same call on both
|
|
29
|
+
* transports — provided the caller supplies the relay node's device signing
|
|
30
|
+
* key, which must be learned out of band and is the one thing that cannot be
|
|
31
|
+
* inferred. See `./relay-observer.ts` for why, and mdma #312 for the hosted
|
|
32
|
+
* case. Without it, {@link MeroClient.canSubscribe} is `false` and the
|
|
33
|
+
* accessors throw naming exactly that.
|
|
34
|
+
*
|
|
35
|
+
* # What is deliberately NOT unified
|
|
36
|
+
*
|
|
37
|
+
* Reads and admin. A relay serves exactly two routes to a caller with no
|
|
38
|
+
* credential — `GET` and `POST /admin-api/contexts/:id/intents` — and while a
|
|
39
|
+
* session obtained by logging in carries `context:query`, `sdk.admin` wants
|
|
40
|
+
* `admin`, which the `account_proof` provider does not grant and a keyholder
|
|
41
|
+
* should never ask for.
|
|
42
|
+
*
|
|
43
|
+
* So those accessors throw on a relay client, with a message naming what is
|
|
44
|
+
* missing, rather than returning something that quietly connects somewhere
|
|
45
|
+
* else. Falling back to "some node" would be the worst of the options: the app
|
|
46
|
+
* would observe a node it never chose, under a credential it never granted, and
|
|
47
|
+
* find out only when the two disagreed. `canSubscribe` and `transport` are
|
|
48
|
+
* there so an app can branch *before* touching any of it.
|
|
49
|
+
*/
|
|
50
|
+
import { MeroJs } from '../mero-js.js';
|
|
51
|
+
import type { MeroJsConfig } from '../mero-js.js';
|
|
52
|
+
import { RelayClient } from '../relay/relay-client.js';
|
|
53
|
+
import type { RelayClientConfig } from '../relay/relay-client.js';
|
|
54
|
+
import type { RelayObserveConfig } from './relay-observer.js';
|
|
55
|
+
import type { ExecuteTransport, TransportKind } from './types.js';
|
|
56
|
+
import type { AdminApiClient } from '../admin-api/index.js';
|
|
57
|
+
import type { ProofCredential } from '../request-proof/index.js';
|
|
58
|
+
import { SseClient } from '../events/sse.js';
|
|
59
|
+
import type { AuthApiClient } from '../auth-api/index.js';
|
|
60
|
+
import type { WsClient } from '../events/ws.js';
|
|
61
|
+
import type { EphemeralClient } from '../ephemeral/index.js';
|
|
62
|
+
import type { CloudClient } from '../cloud/cloud-client.js';
|
|
63
|
+
/** The node transport: everything `createMeroJs` takes, plus an explicit opt-in. */
|
|
64
|
+
export interface NodeTransportConfig extends MeroJsConfig {
|
|
65
|
+
/**
|
|
66
|
+
* Optional, and the default. Spelled out so a config object can be built
|
|
67
|
+
* generically without the node case looking like an omission.
|
|
68
|
+
*/
|
|
69
|
+
transport?: 'node';
|
|
70
|
+
}
|
|
71
|
+
/** The relay transport: delegated execution, no node and no node credential. */
|
|
72
|
+
export interface RelayTransportConfig {
|
|
73
|
+
transport: 'relay';
|
|
74
|
+
/**
|
|
75
|
+
* The relay to write through — a configured {@link RelayClient}, or the
|
|
76
|
+
* config to build one from.
|
|
77
|
+
*
|
|
78
|
+
* An already-built client is accepted because the useful ones are not built
|
|
79
|
+
* by hand: `connectCloud` discovers the relay URL and executor account from a
|
|
80
|
+
* cloud sign-in and hands back a ready client. Requiring the config here
|
|
81
|
+
* would have that caller take the client apart to put it back together.
|
|
82
|
+
*/
|
|
83
|
+
relay: RelayClient | RelayClientConfig;
|
|
84
|
+
/**
|
|
85
|
+
* A session on the relay, if this client has one.
|
|
86
|
+
*
|
|
87
|
+
* The delegated path has **two** credentials, not one. A warrant authorises a
|
|
88
|
+
* single intent and is what `execute` sends; a session — minted by
|
|
89
|
+
* `account_proof` from the same device key, see `login()` — is what the
|
|
90
|
+
* caller-scoped admin READS are authenticated by (`context:list-own`,
|
|
91
|
+
* `namespace:list-own`). They are independent: a client with only warrants
|
|
92
|
+
* writes but cannot list, which is what `admin` throwing used to describe.
|
|
93
|
+
*
|
|
94
|
+
* Supply a token, or a function returning the current one so a refreshed
|
|
95
|
+
* session is picked up without rebuilding the client.
|
|
96
|
+
*/
|
|
97
|
+
session?: string | (() => Promise<string | undefined>);
|
|
98
|
+
/**
|
|
99
|
+
* How this client observes the relay node, if it can.
|
|
100
|
+
*
|
|
101
|
+
* Omit it — or pass it with no `nodeKey` — and the client writes but does not
|
|
102
|
+
* observe: `canSubscribe` is `false` and `events`/`ws` throw naming the
|
|
103
|
+
* missing key. Supply `{ nodeKey }` and `client.events` is the very same
|
|
104
|
+
* `SseClient` a node client hands out.
|
|
105
|
+
*
|
|
106
|
+
* The shape takes a nullable key on purpose, so the hosted path is one code
|
|
107
|
+
* path rather than two: `connectCloud` always passes this object, with
|
|
108
|
+
* whatever key the cloud reported, and starts working the day mdma #312
|
|
109
|
+
* publishes one. Nothing here ever fills the key in.
|
|
110
|
+
*/
|
|
111
|
+
observe?: RelayObserveConfig;
|
|
112
|
+
/**
|
|
113
|
+
* The device cert this client authenticates its own requests with.
|
|
114
|
+
*
|
|
115
|
+
* This is the shortest path to everything but writes, and it needs no session
|
|
116
|
+
* and no node key. The device key signs each request — `X-Calimero-Proof` —
|
|
117
|
+
* and the node verifies the signature against the certificate the account
|
|
118
|
+
* signed, with no store read and no prior relationship. Measured against a
|
|
119
|
+
* node built from core `aecba573b`: the caller-scoped reads answer `200`,
|
|
120
|
+
* `/sse` answers `200`, and a `/ws` upgrade completes `101`.
|
|
121
|
+
*
|
|
122
|
+
* Supplying this makes {@link MeroClient.admin} and {@link MeroClient.events}
|
|
123
|
+
* available and {@link MeroClient.canSubscribe} true, with no `session` and no
|
|
124
|
+
* `observe.nodeKey`. Those two remain for the case they were built for: a
|
|
125
|
+
* device key behind a boundary worth crossing once, where a per-session
|
|
126
|
+
* signature beats a per-request one.
|
|
127
|
+
*/
|
|
128
|
+
proof?: ProofCredential;
|
|
129
|
+
}
|
|
130
|
+
export type MeroClientConfig = NodeTransportConfig | RelayTransportConfig;
|
|
131
|
+
/**
|
|
132
|
+
* A connection whose transport was decided at construction.
|
|
133
|
+
*
|
|
134
|
+
* Everything reachable on a node client is reachable in exactly the shape
|
|
135
|
+
* `MeroJs` exposes it, by delegation rather than re-implementation — this adds
|
|
136
|
+
* no behaviour to the node path and cannot drift from it.
|
|
137
|
+
*/
|
|
138
|
+
export declare class MeroClient {
|
|
139
|
+
/** Which path writes take. Set once, never changes. */
|
|
140
|
+
readonly transport: TransportKind;
|
|
141
|
+
private readonly nodeClient;
|
|
142
|
+
private readonly rpcTransport;
|
|
143
|
+
/** The relay's event session, or `null` on a node client / with no node key. */
|
|
144
|
+
private readonly relayObserver;
|
|
145
|
+
/** Admin reads against the relay, when a session or a proof was supplied. */
|
|
146
|
+
private readonly relayAdmin;
|
|
147
|
+
/**
|
|
148
|
+
* Events off the relay authenticated by a request proof, built lazily.
|
|
149
|
+
*
|
|
150
|
+
* Separate from {@link relayObserver} rather than a mode inside it: the
|
|
151
|
+
* observer's whole subject is a session addressed to a node key, and a proof
|
|
152
|
+
* has neither. Holding the authorizer instead of the client keeps construction
|
|
153
|
+
* free of the first signature.
|
|
154
|
+
*/
|
|
155
|
+
private readonly proofAuthorizer;
|
|
156
|
+
private readonly relayUrl;
|
|
157
|
+
private proofEvents;
|
|
158
|
+
constructor(config: MeroClientConfig);
|
|
159
|
+
/**
|
|
160
|
+
* The write surface. Identical on both transports — this is the whole point.
|
|
161
|
+
*
|
|
162
|
+
* On a node this IS the `RpcClient` a `MeroJs` would have handed out, not a
|
|
163
|
+
* wrapper around it, so `client.rpc` and `client.node.rpc` are the same
|
|
164
|
+
* object.
|
|
165
|
+
*/
|
|
166
|
+
get rpc(): ExecuteTransport;
|
|
167
|
+
/**
|
|
168
|
+
* Whether `events` / `ws` will work on this client.
|
|
169
|
+
*
|
|
170
|
+
* Always `true` on the node transport. On the relay transport it says whether
|
|
171
|
+
* a node key was supplied — the relay can be observed, but only by a caller
|
|
172
|
+
* who can name the key the login statement is signed against.
|
|
173
|
+
*/
|
|
174
|
+
get canSubscribe(): boolean;
|
|
175
|
+
/** The underlying `MeroJs`. Node transport only. */
|
|
176
|
+
get node(): MeroJs;
|
|
177
|
+
/** The underlying `RelayClient`, for `describe()`. Relay transport only. */
|
|
178
|
+
get relay(): RelayClient;
|
|
179
|
+
/**
|
|
180
|
+
* Admin API. Node transport only.
|
|
181
|
+
*
|
|
182
|
+
* Two different things live behind this accessor today, and only one of them
|
|
183
|
+
* is genuinely unavailable through a relay.
|
|
184
|
+
*
|
|
185
|
+
* The **mutations** — `createNamespace`, `setMemberCapabilities`,
|
|
186
|
+
* `upgradeGroup` and the rest — are operator actions on somebody else's
|
|
187
|
+
* machine. A keyholder does not get those, and should not; admin is for
|
|
188
|
+
* admins.
|
|
189
|
+
*
|
|
190
|
+
* The **reads** are a different matter. `getContext`, `getContexts`,
|
|
191
|
+
* `getApplication`, `listNamespaces`, `getBlob` are an app asking what it is
|
|
192
|
+
* looking at, not an operator administering anything. They sit under
|
|
193
|
+
* `/admin-api/` for historical reasons, and a delegated session is refused
|
|
194
|
+
* them (measured: `403`, while the same session reads context state through
|
|
195
|
+
* `POST /contexts/{ctx}/query` and writes through `/intents` quite happily).
|
|
196
|
+
*
|
|
197
|
+
* So this is the one place where "the same code runs on both transports"
|
|
198
|
+
* does not hold yet, and it is a missing surface rather than a missing
|
|
199
|
+
* permission. A transport-independent read API is designed but not built; the
|
|
200
|
+
* throw below is deliberate until it exists, because the alternative — quietly
|
|
201
|
+
* reaching for a node the caller did not choose — is worse than a clear stop.
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* The admin surface.
|
|
205
|
+
*
|
|
206
|
+
* On a node client this is the node's own. On a relay client it exists only
|
|
207
|
+
* when a `session` was supplied, and it is **the relay's admin API
|
|
208
|
+
* authenticated as the account** — so the caller-scoped reads answer:
|
|
209
|
+
* `getContexts` and `listNamespaces` return this account's own, because
|
|
210
|
+
* `account_proof` grants `context:list-own` and `namespace:list-own`.
|
|
211
|
+
*
|
|
212
|
+
* Mutations are not filtered out here, deliberately. A delegated session does
|
|
213
|
+
* not carry the grants they require, so the node refuses them with a `403`
|
|
214
|
+
* that names the missing permission — which is a better answer than a
|
|
215
|
+
* client-side guess, and keeps the policy in the one place that owns it.
|
|
216
|
+
*/
|
|
217
|
+
get admin(): AdminApiClient;
|
|
218
|
+
/** Auth API. Node transport only — a relay issues no node credential. */
|
|
219
|
+
get auth(): AuthApiClient;
|
|
220
|
+
/**
|
|
221
|
+
* SSE events — on **both** transports.
|
|
222
|
+
*
|
|
223
|
+
* On a relay client this is an `SseClient` against the relay node, fed by a
|
|
224
|
+
* session minted with the author's account proof. The session is minted
|
|
225
|
+
* lazily, on the first token the stream asks for, so reading this getter
|
|
226
|
+
* performs no network call.
|
|
227
|
+
*
|
|
228
|
+
* Throws only when the relay client was given no node key: the one input
|
|
229
|
+
* that cannot be inferred. See {@link missingNodeKey}.
|
|
230
|
+
*/
|
|
231
|
+
get events(): SseClient;
|
|
232
|
+
/** WebSocket events, on both transports. @experimental — prefer `events`. */
|
|
233
|
+
get ws(): WsClient;
|
|
234
|
+
/**
|
|
235
|
+
* Ephemeral presence. Node transport only.
|
|
236
|
+
*
|
|
237
|
+
* Not extended to the relay even though a logged-in session could reach both
|
|
238
|
+
* halves of it mechanically. Publishing resolves the author from an *owned
|
|
239
|
+
* context identity on the node*, and whether a delegated session has one has
|
|
240
|
+
* not been established — a presence client that silently published nothing,
|
|
241
|
+
* or published as the wrong identity, is worse than one that says it is not
|
|
242
|
+
* available. The read half is not lost: presence arrives on `client.events`
|
|
243
|
+
* like any other context event, and can be filtered there.
|
|
244
|
+
*
|
|
245
|
+
* The rejected alternative was to expose it over the relay session and let
|
|
246
|
+
* `set` fail at the node. That trades a clear construction-time answer for a
|
|
247
|
+
* runtime failure inside a UI that has already rendered.
|
|
248
|
+
*/
|
|
249
|
+
get ephemeral(): EphemeralClient;
|
|
250
|
+
/**
|
|
251
|
+
* Calimero Cloud.
|
|
252
|
+
*
|
|
253
|
+
* Available on both transports, because it is neither: the cloud session is
|
|
254
|
+
* independent of the node credential and of the relay's signing key, and a
|
|
255
|
+
* relay-transport app still needs it to list namespaces and manage billing.
|
|
256
|
+
*/
|
|
257
|
+
get cloud(): CloudClient;
|
|
258
|
+
/**
|
|
259
|
+
* Release whatever the transport is holding.
|
|
260
|
+
*
|
|
261
|
+
* Safe on a relay client, where there is nothing open to close — a caller
|
|
262
|
+
* that tears down on unmount should not have to know which transport it got.
|
|
263
|
+
*/
|
|
264
|
+
close(): void;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Build a client for the chosen transport. Node unless told otherwise.
|
|
268
|
+
*
|
|
269
|
+
* ```ts
|
|
270
|
+
* // unchanged, and still the default
|
|
271
|
+
* const client = createMeroClient({ baseUrl: 'http://localhost:2428' });
|
|
272
|
+
*
|
|
273
|
+
* // the same call sites, through a relay
|
|
274
|
+
* const client = createMeroClient({
|
|
275
|
+
* transport: 'relay',
|
|
276
|
+
* relay: connection.relay,
|
|
277
|
+
* // the relay node's signing key, learned out of band — without it the
|
|
278
|
+
* // client writes but does not observe
|
|
279
|
+
* observe: { nodeKey },
|
|
280
|
+
* });
|
|
281
|
+
*
|
|
282
|
+
* await client.rpc.execute({ contextId, method: 'set', argsJson: { key, value } });
|
|
283
|
+
* if (client.canSubscribe) client.events.on('event', onEvent);
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
export declare function createMeroClient(config: MeroClientConfig): MeroClient;
|