@calimero-network/mero-js 22.7.0 → 22.9.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.
Files changed (76) hide show
  1. package/dist/account/account.d.ts +99 -4
  2. package/dist/account/account.js +0 -0
  3. package/dist/account/index.d.ts +6 -2
  4. package/dist/account/index.js +3 -1
  5. package/dist/admin-api/admin-client.d.ts +154 -2
  6. package/dist/admin-api/admin-client.js +255 -1
  7. package/dist/admin-api/admin-types.d.ts +193 -5
  8. package/dist/cloud/cloud-client.d.ts +38 -8
  9. package/dist/cloud/cloud-client.js +38 -21
  10. package/dist/cloud/connect.d.ts +101 -2
  11. package/dist/cloud/connect.js +84 -8
  12. package/dist/cloud/enrol-redirect.d.ts +195 -0
  13. package/dist/cloud/enrol-redirect.js +236 -0
  14. package/dist/cloud/index.d.ts +6 -2
  15. package/dist/cloud/index.js +7 -1
  16. package/dist/cloud/link-redirect.d.ts +85 -0
  17. package/dist/cloud/link-redirect.js +105 -0
  18. package/dist/crypto/internal.d.ts +15 -0
  19. package/dist/crypto/internal.js +18 -0
  20. package/dist/device-cert/device-cert.d.ts +96 -2
  21. package/dist/device-cert/device-cert.js +101 -11
  22. package/dist/device-cert/device-scope.d.ts +58 -0
  23. package/dist/device-cert/device-scope.js +76 -0
  24. package/dist/device-cert/index.d.ts +4 -2
  25. package/dist/device-cert/index.js +2 -1
  26. package/dist/events/sse.d.ts +22 -2
  27. package/dist/events/sse.js +20 -11
  28. package/dist/http-client/http-types.d.ts +18 -0
  29. package/dist/http-client/web-client.js +21 -1
  30. package/dist/index.browser.mjs +2 -2
  31. package/dist/index.browser.mjs.map +4 -4
  32. package/dist/index.cjs +1955 -496
  33. package/dist/index.cjs.map +4 -4
  34. package/dist/index.d.ts +13 -4
  35. package/dist/index.js +21 -2
  36. package/dist/index.mjs +1955 -496
  37. package/dist/index.mjs.map +4 -4
  38. package/dist/invitation/index.d.ts +2 -0
  39. package/dist/invitation/index.js +2 -0
  40. package/dist/invitation/invitation.d.ts +106 -0
  41. package/dist/invitation/invitation.js +104 -0
  42. package/dist/login/login.d.ts +11 -1
  43. package/dist/login/login.js +5 -4
  44. package/dist/login/session.d.ts +21 -5
  45. package/dist/login/session.js +7 -15
  46. package/dist/namespace-op/namespace-op.d.ts +14 -2
  47. package/dist/namespace-op/namespace-op.js +13 -19
  48. package/dist/relay/index.d.ts +2 -2
  49. package/dist/relay/index.js +1 -1
  50. package/dist/relay/nonce-source.d.ts +135 -0
  51. package/dist/relay/nonce-source.js +187 -19
  52. package/dist/relay/relay-client.d.ts +60 -5
  53. package/dist/relay/relay-client.js +84 -4
  54. package/dist/relay-attestation/attest-node-key.d.ts +66 -0
  55. package/dist/relay-attestation/attest-node-key.js +110 -0
  56. package/dist/relay-attestation/index.d.ts +1 -0
  57. package/dist/relay-attestation/index.js +1 -0
  58. package/dist/request-proof/authorizer.d.ts +48 -0
  59. package/dist/request-proof/authorizer.js +65 -0
  60. package/dist/request-proof/index.d.ts +4 -0
  61. package/dist/request-proof/index.js +2 -0
  62. package/dist/request-proof/request-proof.d.ts +56 -0
  63. package/dist/request-proof/request-proof.js +111 -0
  64. package/dist/rpc/index.d.ts +21 -0
  65. package/dist/rpc/index.js +20 -0
  66. package/dist/transport/index.d.ts +6 -0
  67. package/dist/transport/index.js +3 -0
  68. package/dist/transport/mero-client.d.ts +286 -0
  69. package/dist/transport/mero-client.js +379 -0
  70. package/dist/transport/relay-observer.d.ts +152 -0
  71. package/dist/transport/relay-observer.js +173 -0
  72. package/dist/transport/relay-transport.d.ts +73 -0
  73. package/dist/transport/relay-transport.js +94 -0
  74. package/dist/transport/types.d.ts +100 -0
  75. package/dist/transport/types.js +32 -0
  76. package/package.json +1 -1
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Reading from the relay — because a relay is a node.
3
+ *
4
+ * # What was wrong with "a relay-transport client cannot observe"
5
+ *
6
+ * The first version of this transport reported `canSubscribe === false` for
7
+ * every relay, on the reasoning that a relay serves only `GET`/`POST
8
+ * /admin-api/contexts/:id/intents`. That is true of the *unauthenticated*
9
+ * surface, and it is the wrong conclusion: a relay is an ordinary node that
10
+ * happens to also run intents for other people. The same origin serves
11
+ * `/auth/challenge`, `/auth/token`, `/sse` and `/ws`, and a keyholder holding
12
+ * an `AccountProof<DeviceCert>` can log in to it with {@link login} — the
13
+ * `account_proof` provider grants exactly `context:intent`, `context:query`,
14
+ * `context:subscribe`. Subscribe is in the provider's own default grant, not
15
+ * something extra that has to be asked for, and a delegated write on such a
16
+ * session was observed arriving as a `StateMutation` frame whose `newRoot`
17
+ * matched the write's `rootHash`.
18
+ *
19
+ * So the gap was never "a relay cannot be observed". It is exactly one missing
20
+ * input.
21
+ *
22
+ * # The one missing input, and why it is not filled in from somewhere
23
+ *
24
+ * {@link login} requires the node's device signing key as a *separately
25
+ * supplied* parameter. It is not read out of the challenge response on purpose:
26
+ * a value learned from the party you are about to authenticate to lets whoever
27
+ * answered choose what your device signs a statement about. That is the whole
28
+ * attack the parameter exists to stop.
29
+ *
30
+ * For a self-hosted relay the operator can hand the key over out of band, and
31
+ * this module works today. For a cloud-hosted relay, `CloudRelay` carries
32
+ * `peerId`, `relayUrl`, `executorAccount`, `status` and `authorshipReady` —
33
+ * and no node key. `peerId` is a libp2p identity and is **not** a substitute:
34
+ * signing a statement about a libp2p peer id would be signing about a different
35
+ * key than the one the node authenticates with. That is filed upstream as mdma
36
+ * #312; until it lands, the hosted case has no key and reports
37
+ * `canSubscribe === false` rather than pretending.
38
+ *
39
+ * Nothing here invents a key, and nothing here falls back to "some other node".
40
+ */
41
+ import { SseClient } from '../events/sse.js';
42
+ import { WsClient } from '../events/ws.js';
43
+ import { login } from '../login/index.js';
44
+ /** Re-login this long before the access token expires, so a reconnect is not racing it. */
45
+ const EXPIRY_SKEW_MS = 30000;
46
+ /**
47
+ * This runtime's honest self-description, used when no audience is given.
48
+ *
49
+ * The origin is taken verbatim from the browser rather than normalized: the
50
+ * node compares it byte for byte, and two spellings of one origin would mean a
51
+ * client that disagrees with itself about which surface a token was for.
52
+ */
53
+ export function defaultAudience() {
54
+ const origin = globalThis.location?.origin;
55
+ return origin ? { kind: 'webOrigin', origin } : { kind: 'cli' };
56
+ }
57
+ /**
58
+ * Milliseconds-since-epoch at which a JWT expires, or `null` if unreadable.
59
+ *
60
+ * Deliberately `null` rather than a fallback guess. `mero-js.ts` has a similar
61
+ * helper that substitutes "an hour from now" when it cannot read `exp`, which
62
+ * is right for a token bundle that will be refreshed reactively on a 401 and
63
+ * wrong here: an unreadable lifetime must mean "mint a fresh session", not
64
+ * "assume an hour and let a dead token take the stream down".
65
+ */
66
+ function jwtExpiryMs(token) {
67
+ try {
68
+ const parts = token.split('.');
69
+ if (parts.length !== 3)
70
+ return null;
71
+ let b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
72
+ while (b64.length % 4)
73
+ b64 += '=';
74
+ const payload = JSON.parse(atob(b64));
75
+ return typeof payload.exp === 'number' ? payload.exp * 1000 : null;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * A node session over the relay, and the event clients it feeds.
83
+ *
84
+ * The session is minted lazily, on the first thing that actually needs a token.
85
+ * That is not an optimization: `client.events` is a *getter*, so constructing a
86
+ * client would otherwise have to perform two HTTP calls and a signature before
87
+ * anyone had asked to observe anything.
88
+ */
89
+ export class RelayObserver {
90
+ constructor(config) {
91
+ this.session = null;
92
+ this.sessionExpiresAt = null;
93
+ /** In-flight login, so two surfaces asking at once mint one session, not two. */
94
+ this.pending = null;
95
+ this.sseClient = null;
96
+ this.wsClient = null;
97
+ this.config = config;
98
+ }
99
+ /** The node key this observer logs in with. Read for diagnostics. */
100
+ get nodeKey() {
101
+ return this.config.nodeKey;
102
+ }
103
+ /**
104
+ * A usable access token, logging in if there is not one.
105
+ *
106
+ * Re-mints once the current token is within {@link EXPIRY_SKEW_MS} of
107
+ * expiring, and on every call when the token's lifetime cannot be read —
108
+ * reconnects are backed off, so an extra handshake costs far less than a
109
+ * stream that silently stops. The refresh token is *held* but not spent:
110
+ * rotating it is tracked in #87/#88 and a second login is correct in the
111
+ * meantime, where re-presenting an unrotated refresh token would not be.
112
+ */
113
+ async accessToken() {
114
+ const now = Date.now();
115
+ if (this.session &&
116
+ this.sessionExpiresAt !== null &&
117
+ this.sessionExpiresAt - EXPIRY_SKEW_MS > now) {
118
+ return this.session.accessToken;
119
+ }
120
+ if (!this.pending) {
121
+ this.pending = this.loginNow().finally(() => {
122
+ this.pending = null;
123
+ });
124
+ }
125
+ const session = await this.pending;
126
+ return session.accessToken;
127
+ }
128
+ async loginNow() {
129
+ const session = await login({
130
+ nodeUrl: this.config.nodeUrl,
131
+ node: this.config.nodeKey,
132
+ signer: await this.config.signer(),
133
+ accountProof: this.config.accountProof,
134
+ audience: this.config.audience,
135
+ clientName: this.config.clientName,
136
+ ttlSeconds: this.config.ttlSeconds,
137
+ fetch: this.config.fetch,
138
+ timeoutMs: this.config.timeoutMs,
139
+ });
140
+ this.session = session;
141
+ this.sessionExpiresAt = jwtExpiryMs(session.accessToken);
142
+ return session;
143
+ }
144
+ /**
145
+ * SSE events over the relay node — the same `SseClient` a node client hands
146
+ * out, constructed the same way, so nothing downstream can tell which
147
+ * transport produced it.
148
+ */
149
+ get events() {
150
+ if (!this.sseClient) {
151
+ this.sseClient = new SseClient({
152
+ baseUrl: this.config.nodeUrl,
153
+ getAuthToken: () => this.accessToken(),
154
+ });
155
+ }
156
+ return this.sseClient;
157
+ }
158
+ /** WebSocket events over the relay node. @experimental — prefer `events`. */
159
+ get ws() {
160
+ if (!this.wsClient) {
161
+ this.wsClient = new WsClient({
162
+ baseUrl: this.config.nodeUrl,
163
+ getAuthToken: () => this.accessToken(),
164
+ });
165
+ }
166
+ return this.wsClient;
167
+ }
168
+ /** Close whatever was opened. Never touches the relay's write path. */
169
+ close() {
170
+ this.sseClient?.close();
171
+ this.wsClient?.close();
172
+ }
173
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The relay, wearing the node's `execute` shape.
3
+ *
4
+ * This is an adapter and nothing else: it owns no protocol, mints no warrant
5
+ * and holds no key. `RelayClient` does all of that and stays the place to look
6
+ * when a write is refused. What lives here is the two differences that stopped
7
+ * an app from swapping one for the other — named arguments instead of
8
+ * positional, and `T` instead of `IntentResult<T>`.
9
+ *
10
+ * The unwrap is the load-bearing part: `IntentResult<T>` becomes `T`, so a call
11
+ * site cannot tell which transport ran it.
12
+ *
13
+ * A method's `null` return is passed through as `null` rather than normalized
14
+ * to `undefined`. That looks like the less tidy choice and is the correct one:
15
+ * `RpcClient.execute` returns `result.output` verbatim, so a node method whose
16
+ * output is `null` already resolves to `null` today. Rewriting the relay's
17
+ * `null` would make the two transports disagree in exactly the case this
18
+ * adapter exists to make agree. The alternative — normalizing *both* sides to
19
+ * `undefined` — was rejected because it changes what shipped node call sites
20
+ * see, which is the one thing this work must not do.
21
+ *
22
+ * The `rootHash` the relay also reports is NOT discarded — it comes back from
23
+ * {@link RelayTransport.executeWithMetadata}.
24
+ */
25
+ import type { ExecuteParams, MigrateMyEntriesSummary } from '../rpc/index.js';
26
+ import type { RelayClient } from '../relay/relay-client.js';
27
+ import type { ExecuteResult, ExecuteTransport, TransportKind } from './types.js';
28
+ import type { RelayObserver } from './relay-observer.js';
29
+ export declare class RelayTransport implements ExecuteTransport {
30
+ private readonly relay;
31
+ /**
32
+ * How this transport observes, or `null` when nobody supplied the relay
33
+ * node's signing key. Held here rather than on `MeroClient` so that
34
+ * {@link RelayTransport.canSubscribe} — which an app may read off
35
+ * `client.rpc` — answers about the thing it is actually asked about.
36
+ */
37
+ private readonly observer;
38
+ readonly kind: TransportKind;
39
+ constructor(relay: RelayClient,
40
+ /**
41
+ * How this transport observes, or `null` when nobody supplied the relay
42
+ * node's signing key. Held here rather than on `MeroClient` so that
43
+ * {@link RelayTransport.canSubscribe} — which an app may read off
44
+ * `client.rpc` — answers about the thing it is actually asked about.
45
+ */
46
+ observer?: RelayObserver | null);
47
+ /**
48
+ * Whether this relay can be observed — a fact, not a constant.
49
+ *
50
+ * A relay IS a node: it serves `/auth/challenge`, `/auth/token`, `/sse` and
51
+ * `/ws`, and the `account_proof` provider grants `context:subscribe` by
52
+ * default. What decides the answer is therefore not the transport but whether
53
+ * a caller supplied the node's device signing key, which cannot be inferred
54
+ * from the node or from its `peerId`. See `./relay-observer.ts`.
55
+ */
56
+ get canSubscribe(): boolean;
57
+ /** The event session, or `null` when no node key was supplied. */
58
+ get events(): RelayObserver | null;
59
+ /** The underlying client, for `describe()` and anything else relay-specific. */
60
+ get client(): RelayClient;
61
+ execute<T = unknown>(params: ExecuteParams): Promise<T>;
62
+ executeWithMetadata<T = unknown>(params: ExecuteParams): Promise<ExecuteResult<T>>;
63
+ /**
64
+ * Both of these are plain context methods, so they work through a relay
65
+ * exactly as they do on a node — the warrant names `migrate_my_entries` and
66
+ * the delta is attributed to the author, which is precisely the identity the
67
+ * convert is scoped to. They are re-declared rather than inherited because
68
+ * `ExecuteTransport` is an interface, not a base class.
69
+ */
70
+ migrateMyEntries(contextId: string): Promise<MigrateMyEntriesSummary>;
71
+ /** Read-only count of the caller's entries still below the target schema. */
72
+ countMyPending(contextId: string): Promise<number>;
73
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The relay, wearing the node's `execute` shape.
3
+ *
4
+ * This is an adapter and nothing else: it owns no protocol, mints no warrant
5
+ * and holds no key. `RelayClient` does all of that and stays the place to look
6
+ * when a write is refused. What lives here is the two differences that stopped
7
+ * an app from swapping one for the other — named arguments instead of
8
+ * positional, and `T` instead of `IntentResult<T>`.
9
+ *
10
+ * The unwrap is the load-bearing part: `IntentResult<T>` becomes `T`, so a call
11
+ * site cannot tell which transport ran it.
12
+ *
13
+ * A method's `null` return is passed through as `null` rather than normalized
14
+ * to `undefined`. That looks like the less tidy choice and is the correct one:
15
+ * `RpcClient.execute` returns `result.output` verbatim, so a node method whose
16
+ * output is `null` already resolves to `null` today. Rewriting the relay's
17
+ * `null` would make the two transports disagree in exactly the case this
18
+ * adapter exists to make agree. The alternative — normalizing *both* sides to
19
+ * `undefined` — was rejected because it changes what shipped node call sites
20
+ * see, which is the one thing this work must not do.
21
+ *
22
+ * The `rootHash` the relay also reports is NOT discarded — it comes back from
23
+ * {@link RelayTransport.executeWithMetadata}.
24
+ */
25
+ export class RelayTransport {
26
+ constructor(relay,
27
+ /**
28
+ * How this transport observes, or `null` when nobody supplied the relay
29
+ * node's signing key. Held here rather than on `MeroClient` so that
30
+ * {@link RelayTransport.canSubscribe} — which an app may read off
31
+ * `client.rpc` — answers about the thing it is actually asked about.
32
+ */
33
+ observer = null) {
34
+ this.relay = relay;
35
+ this.observer = observer;
36
+ this.kind = 'relay';
37
+ }
38
+ /**
39
+ * Whether this relay can be observed — a fact, not a constant.
40
+ *
41
+ * A relay IS a node: it serves `/auth/challenge`, `/auth/token`, `/sse` and
42
+ * `/ws`, and the `account_proof` provider grants `context:subscribe` by
43
+ * default. What decides the answer is therefore not the transport but whether
44
+ * a caller supplied the node's device signing key, which cannot be inferred
45
+ * from the node or from its `peerId`. See `./relay-observer.ts`.
46
+ */
47
+ get canSubscribe() {
48
+ return this.observer !== null;
49
+ }
50
+ /** The event session, or `null` when no node key was supplied. */
51
+ get events() {
52
+ return this.observer;
53
+ }
54
+ /** The underlying client, for `describe()` and anything else relay-specific. */
55
+ get client() {
56
+ return this.relay;
57
+ }
58
+ async execute(params) {
59
+ const { returns } = await this.executeWithMetadata(params);
60
+ return returns;
61
+ }
62
+ async executeWithMetadata(params) {
63
+ const result = await this.relay.execute(params.contextId, params.method,
64
+ // The node defaults a missing `argsJson` to `{}`; matching that here keeps
65
+ // a zero-argument call identical on both transports rather than sending
66
+ // `undefined` and letting the relay's JSON body decide.
67
+ params.argsJson ?? {});
68
+ return {
69
+ // Verbatim. `RelayClient` has already normalized a missing `returns` to
70
+ // `null`, which is what a node reports for a null output — see the
71
+ // module note on why this is not rewritten to `undefined`.
72
+ returns: result.returns,
73
+ transport: 'relay',
74
+ rootHash: result.rootHash,
75
+ };
76
+ }
77
+ /**
78
+ * Both of these are plain context methods, so they work through a relay
79
+ * exactly as they do on a node — the warrant names `migrate_my_entries` and
80
+ * the delta is attributed to the author, which is precisely the identity the
81
+ * convert is scoped to. They are re-declared rather than inherited because
82
+ * `ExecuteTransport` is an interface, not a base class.
83
+ */
84
+ async migrateMyEntries(contextId) {
85
+ return this.execute({
86
+ contextId,
87
+ method: 'migrate_my_entries',
88
+ });
89
+ }
90
+ /** Read-only count of the caller's entries still below the target schema. */
91
+ async countMyPending(contextId) {
92
+ return this.execute({ contextId, method: 'count_my_pending' });
93
+ }
94
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * One call shape, two ways of getting there.
3
+ *
4
+ * # Why the node shape is the canonical one
5
+ *
6
+ * `RpcClient.execute({ contextId, method, argsJson })` is what every app in the
7
+ * fleet already writes, and `RelayClient.execute(contextId, method, argsJson)`
8
+ * is what the delegated path grew independently. They differ in *both* halves —
9
+ * positional vs. named arguments, and `T` vs. `IntentResult<T>`. A caller that
10
+ * wanted to switch transports therefore had to rewrite every call site, which
11
+ * makes the transport a property of the application rather than of the
12
+ * connection.
13
+ *
14
+ * This interface closes that by declaring the node's shape the contract and
15
+ * adapting the relay to it. The direction is deliberate and not symmetric:
16
+ * shipped code uses the node shape, so any other choice would be a breaking
17
+ * change dressed up as an abstraction.
18
+ *
19
+ * # Why subscription is not a method on this interface
20
+ *
21
+ * Not because a relay cannot be observed — it can; a relay is an ordinary node
22
+ * and a keyholder can log in to it (see `./relay-observer.ts`). It is because
23
+ * `RpcClient` satisfies this interface *as it already ships*, and `RpcClient`
24
+ * has never had a `subscribe`. Adding one would change the canonical shape,
25
+ * which is the single thing this work must not do.
26
+ *
27
+ * Observation therefore lives on `MeroClient` — `client.events`, `client.ws` —
28
+ * where it lives for a node client, and is the same call on both transports.
29
+ * {@link ExecuteTransport.canSubscribe} reports whether it will work, so an app
30
+ * can branch on data rather than on a thrown error.
31
+ */
32
+ import type { ExecuteParams, MigrateMyEntriesSummary } from '../rpc/index.js';
33
+ /** Which path a write takes. `'node'` is the default everywhere. */
34
+ export type TransportKind = 'node' | 'relay';
35
+ /**
36
+ * A method's return value plus whatever the transport knew about the write.
37
+ *
38
+ * The canonical `execute` returns `T` and nothing else, because that is what
39
+ * every existing call site expects. The relay, though, also learns the
40
+ * context's scope root after the run — the one cheap way to tell "this changed
41
+ * something" from "this was a no-op" — and discarding it to fit the node shape
42
+ * would lose information the node genuinely does not have.
43
+ *
44
+ * So it is carried here, on a second method, rather than on a mutable
45
+ * `lastIntent` accessor. That alternative was rejected on purpose: two
46
+ * `execute` calls in flight at once would race for the same slot, and the
47
+ * caller reading it has no way to tell whose answer it got.
48
+ */
49
+ export interface ExecuteResult<T = unknown> {
50
+ /** Exactly what `execute` would have returned. */
51
+ returns: T;
52
+ /** Which transport ran it. */
53
+ transport: TransportKind;
54
+ /**
55
+ * The context's scope root after the run, hex — relay only.
56
+ *
57
+ * Absent on the node transport because JSON-RPC `execute` does not report it,
58
+ * not because it was dropped. `undefined` therefore means "unknown here", and
59
+ * must never be read as "nothing changed".
60
+ */
61
+ rootHash?: string;
62
+ }
63
+ /**
64
+ * The write surface an app codes against, whichever transport is underneath.
65
+ *
66
+ * `RpcClient` satisfies this as-is — that is the point of picking its shape —
67
+ * so an app already holding one is already holding an `ExecuteTransport`.
68
+ */
69
+ export interface ExecuteTransport {
70
+ /** Which path this is. Useful for diagnostics and for feature-gating UI. */
71
+ readonly kind: TransportKind;
72
+ /**
73
+ * Whether this transport can observe events **as configured**.
74
+ *
75
+ * Always `true` on the node transport, which holds a credential on the node
76
+ * it is pointed at. On the relay transport it depends on whether the caller
77
+ * supplied the relay node's device signing key: the relay serves `/sse` and
78
+ * `/ws` like any node, but a login statement must be signed against a key
79
+ * learned out of band, and that key cannot be read from the node or derived
80
+ * from its `peerId`.
81
+ *
82
+ * Reported rather than attempted, so an app branches before it subscribes.
83
+ * `false` never means "try anyway and we'll connect to something else" —
84
+ * nothing here will silently pick a node the app did not choose.
85
+ */
86
+ readonly canSubscribe: boolean;
87
+ /** Run one method in one context. The canonical signature. */
88
+ execute<T = unknown>(params: ExecuteParams): Promise<T>;
89
+ /**
90
+ * The same call, with whatever the transport knows about the write.
91
+ *
92
+ * Use it when the extra metadata matters; `execute` stays the one an app
93
+ * reaches for, so that switching transports changes no call site.
94
+ */
95
+ executeWithMetadata<T = unknown>(params: ExecuteParams): Promise<ExecuteResult<T>>;
96
+ /** One-tap owner-driven convert. See `RpcClient.migrateMyEntries`. */
97
+ migrateMyEntries(contextId: string): Promise<MigrateMyEntriesSummary>;
98
+ /** Read-only count of the caller's entries still below the target schema. */
99
+ countMyPending(contextId: string): Promise<number>;
100
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * One call shape, two ways of getting there.
3
+ *
4
+ * # Why the node shape is the canonical one
5
+ *
6
+ * `RpcClient.execute({ contextId, method, argsJson })` is what every app in the
7
+ * fleet already writes, and `RelayClient.execute(contextId, method, argsJson)`
8
+ * is what the delegated path grew independently. They differ in *both* halves —
9
+ * positional vs. named arguments, and `T` vs. `IntentResult<T>`. A caller that
10
+ * wanted to switch transports therefore had to rewrite every call site, which
11
+ * makes the transport a property of the application rather than of the
12
+ * connection.
13
+ *
14
+ * This interface closes that by declaring the node's shape the contract and
15
+ * adapting the relay to it. The direction is deliberate and not symmetric:
16
+ * shipped code uses the node shape, so any other choice would be a breaking
17
+ * change dressed up as an abstraction.
18
+ *
19
+ * # Why subscription is not a method on this interface
20
+ *
21
+ * Not because a relay cannot be observed — it can; a relay is an ordinary node
22
+ * and a keyholder can log in to it (see `./relay-observer.ts`). It is because
23
+ * `RpcClient` satisfies this interface *as it already ships*, and `RpcClient`
24
+ * has never had a `subscribe`. Adding one would change the canonical shape,
25
+ * which is the single thing this work must not do.
26
+ *
27
+ * Observation therefore lives on `MeroClient` — `client.events`, `client.ws` —
28
+ * where it lives for a node client, and is the same call on both transports.
29
+ * {@link ExecuteTransport.canSubscribe} reports whether it will work, so an app
30
+ * can branch on data rather than on a thrown error.
31
+ */
32
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "22.7.0",
3
+ "version": "22.9.0",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {