@calimero-network/mero-js 29.14.0 → 29.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.
package/README.md CHANGED
@@ -111,7 +111,7 @@ Pinned measurements go through `policy.verifyQuote` (`createQuoteVerifier`).
111
111
  - **Any route**: `get`, `post`, `put`, `delete`; `postConfirmed` and `deleteConfirmed` answer a `412 CONFIRM_REQUIRED` with a passkey.
112
112
 
113
113
  Refusals are typed (`LoginRequired`, `PasskeyUnknownHere`, `WrongNode`, `NotFound`, `ConfirmRequired`, `ConfirmCancelled`, `ConfirmPromptBlocked`,
114
- `ArgsTooLarge`, `IdempotencyMismatch`, `ReleaseDeadlineError`, `ConsentStale`, `AppError`, `TrapError`,
114
+ `ArgsTooLarge`, `IdempotencyMismatch`, `ReleaseDeadlineError`, `RuntimeTooOld`, `ConsentStale`, `AppError`, `TrapError`,
115
115
  `UnavailableError`, `OriginNotRegistered`, `BadOrigin`, `OriginTaken`, `QuotaExceeded`),
116
116
  all extending `CalimeroError` with the node's `status` and `code`. A `401` once
117
117
  the node that issued the session is replaced or out of reach is answered by one
@@ -125,6 +125,41 @@ The pieces it is built from are exported too: the sealed transport
125
125
  the client for mero-tee's private agent gate (`attestAgent`, `provisionAgent`,
126
126
  `releaseAgent`).
127
127
 
128
+ ## The runtime and the route layer
129
+
130
+ The client is two layers. The **runtime** (`@calimero-network/mero-js/runtime`,
131
+ `src/runtime/`) is everything that is not a route: the node's attestation, the
132
+ sealed channel, the credential and the session's refresh on a `401`, moving on
133
+ `421 WRONG_NODE`, D70 retries and failover, the `412 CONFIRM_REQUIRED` passkey
134
+ step-up, error mapping and server-sent events. Its whole surface toward routes is
135
+ one interface:
136
+
137
+ ```ts
138
+ interface Transport {
139
+ request(method: HttpMethod, path: string, options?: RequestOptions): Promise<Response>; // 2xx, body unread
140
+ json<T>(method: HttpMethod, path: string, options?: RequestOptions): Promise<T>;
141
+ events(path: string, options?: { query?; signal? }): AsyncGenerator<CloudEvent>;
142
+ }
143
+ // RequestOptions: { query?, body? (JSON) | bytes? + contentType?, passkey? (answers a 412),
144
+ // userVerification?, retry?: true | 'once' (D70), signal?, promptSignal? }
145
+ ```
146
+
147
+ A refusal is thrown as its typed `CalimeroError`. `NodeTransport` implements it;
148
+ `routePath('apps/{app}/blobs/{id}', { app, id })` fills a path with encoded
149
+ segments. The **route layer**, `CalimeroClient`, is the hand-written methods on
150
+ top; `client.transport` is its runtime, so a route layer of your own (one
151
+ generated from the node's OpenAPI document, say) shares the client's session:
152
+
153
+ ```ts
154
+ import { routePath } from '@calimero-network/mero-js/runtime';
155
+
156
+ const app = await client.transport.json('GET', routePath('apps/{app}', { app: appId }));
157
+ await client.transport.json('DELETE', routePath('orgs/{org}', { org }), { body: {}, passkey });
158
+ ```
159
+
160
+ The `/runtime` entry re-exports the package's own bundle, so its error classes are
161
+ the ones `CalimeroClient` throws (`instanceof` holds across both imports).
162
+
128
163
  ## Development
129
164
 
130
165
  ```bash
@@ -82,7 +82,28 @@ export interface ReportedReplica {
82
82
  release: string | null;
83
83
  approved: boolean;
84
84
  }
85
- /** One app in a report. `name`, `team` and `availableVersions` are in members' and accounts' reports only. */
85
+ /** A replica's node whose runtime is older than a release needs, and what it runs: the version, or `{ before }` for a node on a release from before nodes named their runtime. */
86
+ export interface RuntimeShortNode {
87
+ node: number;
88
+ runtime: string | {
89
+ before: string;
90
+ } | null;
91
+ }
92
+ /**
93
+ * Why an app cannot move to the newest release it may see (core's
94
+ * `latestBlocked`): `runtime`, a replica's node runs an older runtime than
95
+ * the release's `minRuntimeVersion` (the upgrade would be refused `409
96
+ * RUNTIME_TOO_OLD`); `authority`, the app's owner upgrades it and the
97
+ * release would give its upgrades to the publisher.
98
+ */
99
+ export interface LatestBlocked {
100
+ version: string;
101
+ reason: 'runtime' | 'authority';
102
+ message: string;
103
+ minRuntimeVersion?: string;
104
+ nodes?: RuntimeShortNode[];
105
+ }
106
+ /** One app in a report. `name`, `team`, `availableVersions`, `latestReachable` and `latestBlocked` are in members' and accounts' reports only. */
86
107
  export interface ReportedApp {
87
108
  app: string;
88
109
  group: number;
@@ -103,6 +124,14 @@ export interface ReportedApp {
103
124
  team?: string;
104
125
  /** Newer releases of its package it may be upgraded to, oldest first (at most 16, the newest). */
105
126
  availableVersions?: string[];
127
+ /**
128
+ * The newest of every newer release it may see that an upgrade would take
129
+ * now, which may be older than all of `availableVersions`; `null` when none
130
+ * would. Absent from a node before core 1.0.0-rc.16.
131
+ */
132
+ latestReachable?: string | null;
133
+ /** Present when the newest release it may see is not `latestReachable`: which, and why. */
134
+ latestBlocked?: LatestBlocked;
106
135
  }
107
136
  /** A mero-tee release the cluster's nodes run, with how many and whether the scope approved it. */
108
137
  export interface ClusterRelease {
@@ -308,8 +337,16 @@ export interface PendingUpgrade {
308
337
  name: string | null;
309
338
  package: string;
310
339
  from: string;
311
- /** The newest it may move to. */
340
+ /**
341
+ * The newest it can move to now (the report's `latestReachable`); when
342
+ * none can be (`reachable: false`), or from a node that does not say, the
343
+ * newest it may see.
344
+ */
312
345
  to: string;
346
+ /** Whether an upgrade to `to` would be taken now; `false`: `blocked` says why none is. */
347
+ reachable: boolean;
348
+ /** Why the newest release it may see is not reachable, when it is not. */
349
+ blocked: LatestBlocked | null;
313
350
  /** Every newer release it may move to, oldest first. */
314
351
  available: string[];
315
352
  /** Whether owners hold app upgrades on this cluster; the node checks the package's own `upgrades` when asked. */
@@ -140,12 +140,16 @@ export function pendingApprovalsFrom(report, rules, now = Date.now() / 1000) {
140
140
  const available = a.availableVersions ?? [];
141
141
  if (a.package === null || available.length === 0)
142
142
  continue;
143
+ const latest = available[available.length - 1];
144
+ const reachable = a.latestReachable === undefined ? latest : a.latestReachable;
143
145
  upgrades.push({
144
146
  app: a.app,
145
147
  name: a.name ?? null,
146
148
  package: a.package.package,
147
149
  from: a.package.version,
148
- to: available[available.length - 1],
150
+ to: reachable ?? latest,
151
+ reachable: reachable !== null,
152
+ blocked: a.latestBlocked ?? null,
149
153
  available,
150
154
  actionable: upgradesActionable,
151
155
  });
@@ -9,17 +9,13 @@
9
9
  * const { result } = await cloud.call(appId, 'set', { key: 'a', value: '1' });
10
10
  * ```
11
11
  */
12
+ import { DEFAULT_RETRY, NodeTransport, retryDelayMs, type CalimeroSession, type CloudEvent, type CloudRetryOptions } from '../runtime/transport.js';
12
13
  import { type AttestationDoc, type CloudPolicy } from './attestation.js';
13
14
  import { type PasskeyProvider } from './passkey.js';
14
15
  import { type AppsReport, type ConsentRequest, type ConsentResult, type ConsentStarted, type OpenConsentOptions, type OpsAppsReport, type OpsOrgsReport, type PageOptions, type PendingApprovals, type PublisherBindingInfo, type PublisherProof, type ReleaseRules } from './approvals.js';
15
- export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmPromptBlocked, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, PasskeyUnknownHere, QuotaExceeded, RecoveryNotReady, ReleaseDeadlineError, SignUpRefused, TrapError, UnavailableError, WrongNode, WrongRecoveryCode, errorFrom, } from './errors.js';
16
+ export { AppError, ArgsTooLarge, BadOrigin, CalimeroError, ConfirmCancelled, ConfirmPromptBlocked, ConfirmRequired, IdempotencyMismatch, LoginRequired, ConsentStale, NotFound, OriginNotRegistered, OriginTaken, PasskeyUnknownHere, QuotaExceeded, RecoveryNotReady, ReleaseDeadlineError, RuntimeTooOld, SignUpRefused, TrapError, UnavailableError, WrongNode, WrongRecoveryCode, errorFrom, } from './errors.js';
16
17
  export type { Committed, SignUpRefusal } from './errors.js';
17
- export interface CalimeroSession {
18
- accountId: string;
19
- sessionToken: string;
20
- expiresAt: number;
21
- refreshToken: string;
22
- }
18
+ export type { CalimeroSession, CloudEvent };
23
19
  export interface CallResult {
24
20
  index: number;
25
21
  result: unknown;
@@ -32,11 +28,6 @@ export interface ViewResult {
32
28
  index: number;
33
29
  result: unknown;
34
30
  }
35
- export interface CloudEvent {
36
- id: string;
37
- kind: string;
38
- data: unknown;
39
- }
40
31
  /** What installing a package release answers. */
41
32
  export interface PackageInstalled {
42
33
  package: string;
@@ -703,31 +694,8 @@ export interface SignUpPolicy {
703
694
  /** Whether a sign-up without an invite is refused. */
704
695
  inviteRequired: boolean;
705
696
  }
706
- /**
707
- * How a call or a view is sent again while the node is unavailable (D70): with
708
- * the same key, after a backoff that starts at `baseDelayMs` and doubles up to
709
- * `maxDelayMs`, no jitter, at most `retries` times. The defaults are the Rust
710
- * client's: 6 retries, 200 ms doubling, at most 2 s (about 9 s in all, past a
711
- * leader election).
712
- */
713
- export interface CloudRetryOptions {
714
- /** Retries after the first attempt. Default 6. */
715
- retries?: number;
716
- /** The first backoff; each retry doubles it. Default 200 ms. */
717
- baseDelayMs?: number;
718
- /** The longest backoff. Default 2 s. */
719
- maxDelayMs?: number;
720
- }
721
- export declare const DEFAULT_RETRY: {
722
- readonly retries: 6;
723
- readonly baseDelayMs: 200;
724
- readonly maxDelayMs: 2000;
725
- };
726
- /** The backoff before retry `retry` (0 for the first): `baseDelayMs` doubling, at most `maxDelayMs`. */
727
- export declare function retryDelayMs(retry: number, options: {
728
- baseDelayMs: number;
729
- maxDelayMs: number;
730
- }): number;
697
+ export { DEFAULT_RETRY, retryDelayMs };
698
+ export type { CloudRetryOptions };
731
699
  export interface CalimeroClientOptions {
732
700
  /** One node of the cluster: the one the client talks to first. */
733
701
  baseUrl: string;
@@ -763,24 +731,15 @@ export interface CalimeroConnectOptions {
763
731
  random?: () => number;
764
732
  }
765
733
  export declare class CalimeroClient {
766
- /** Every node this client may use, `baseUrl` first (or as the directory last ordered them). */
767
- private nodeUrls;
768
- /** The one in use: an index into {@link nodeUrls}. */
769
- private at;
770
- /** A sealed fetch per node, each attesting its own node on first use. */
771
- private readonly channels;
772
- private readonly policy;
773
- /** The unsealed fetch, for the one route served in the clear (`/attestation`). */
774
- private readonly raw;
775
- private auth;
776
- private current;
777
- /** The refresh under way, which every request that needs one shares. */
778
- private refreshing;
779
- private readonly retry;
780
- /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
781
- private readonly networkFailures;
782
- /** The directory the node list came from, for a client made by {@link connect}. */
783
- private source;
734
+ /**
735
+ * The runtime every route of this client is sent through
736
+ * (`@calimero-network/mero-js/runtime`): the attested, sealed channel to each
737
+ * node, the credential and its refresh, the `412 CONFIRM_REQUIRED` step-up,
738
+ * D70 retries and error mapping. A route layer of its own (one generated from
739
+ * the node's OpenAPI document, say) sends through it and shares this
740
+ * client's session.
741
+ */
742
+ readonly transport: NodeTransport;
784
743
  /**
785
744
  * A client for the Calimero Cloud cluster named `cluster`: its node list read
786
745
  * from the public node directory, those in `zone` first, each group in
@@ -792,87 +751,18 @@ export declare class CalimeroClient {
792
751
  * first request once the directory's `refreshAfterSeconds` has passed.
793
752
  */
794
753
  static connect(options: CalimeroConnectOptions): Promise<CalimeroClient>;
795
- constructor(options: CalimeroClientOptions);
754
+ /** A client of the nodes `options` names, or one sending through an existing `transport`. */
755
+ constructor(options: CalimeroClientOptions | NodeTransport);
796
756
  /** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` or a failover moved the client on. */
797
757
  get nodeUrl(): string;
798
758
  /** Every node this client may use, in the order it moves through them. */
799
759
  get nodes(): string[];
800
- /** The sealed channel to `url`, attested under the client's policy before its first request. */
801
- private channel;
802
- /**
803
- * Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
804
- * next one. False when there is no other. A request that saw the same node
805
- * refuse after another request moved on already does not move again.
806
- */
807
- private moveOn;
808
- /**
809
- * Fail over from node `from`, where a request's retries ran out, to the next
810
- * node it has not `tried`. False when it has tried them all. A request whose
811
- * node another request moved away from already goes to that node, if untried.
812
- */
813
- private failOver;
814
- /**
815
- * Read the node list again from the directory, in the background, for a
816
- * client made by {@link connect}: the node in use stays first, then the
817
- * directory's order; a node no longer listed is dropped. A failed read is
818
- * ignored. One read at a time.
819
- */
820
- private reread;
821
760
  /** Authenticate every request with an API key. */
822
761
  useApiKey(key: string): void;
823
762
  useSession(session: CalimeroSession): void;
824
763
  get session(): CalimeroSession | undefined;
825
- /**
826
- * Send one request to the node in use, with the credential held now, and
827
- * answer the response, whatever its status. {@link send} judges it.
828
- */
829
- private request;
830
- /**
831
- * {@link request}, answering what a lost issuing node or a node on the
832
- * wrong release does to a session: a successful response, or the refusal as a
833
- * typed error ({@link errorFrom}).
834
- *
835
- * - `401 UNAUTHENTICATED` to a request that carried a session: the node that
836
- * signed the session may be gone, replaced, or out of reach of the node
837
- * asked, which any node of the cluster answers with a refresh. The client
838
- * refreshes once, shared by every request that needs it at the same time
839
- * ({@link refresh}), and repeats the request once. A refresh that is
840
- * refused surfaces as {@link LoginRequired}.
841
- * - `421 WRONG_NODE`: this node's release may not serve the account. The
842
- * client moves to the next node it knows and repeats the request once; with
843
- * none, the {@link WrongNode} reaches the caller.
844
- *
845
- * Repeating is safe even for a request that is not idempotent: the node
846
- * answers `401` and `421` when it judges the credential, before it reads what
847
- * the request asks, so the first attempt ran nothing. The exceptions are the
848
- * ceremonies that mint a session ({@link ceremony}), never repeated here.
849
- */
850
- private send;
851
- /** Whether a `401` to a request sent with `token` is one a refresh may answer. */
852
- private mayRefresh;
853
- /**
854
- * A fresh session in place of the one `token` belonged to: by the refresh
855
- * another request already made, or by one this request starts.
856
- */
857
- private renew;
764
+ /** A JSON request, or a raw one when `raw` is given (as `contentType`, the transport's default when absent). */
858
765
  private json;
859
- /**
860
- * {@link json} for a call or a view (D70): while the node answers 503 or
861
- * cannot be reached, the same request, with the same idempotency key, is sent
862
- * again after {@link retryDelayMs} (or the pause the node asked for, if
863
- * longer), at most `retries` times. Every other answer is final.
864
- *
865
- * When a node's retries run out, the request fails over to the next node the
866
- * client knows and its retries start again there, with the same key: the
867
- * key is kept in the app's replicated log, so the cluster answers it once
868
- * whichever node it reaches. Each node is tried once per request. The last
869
- * failure surfaces as {@link UnavailableError}: the node's 503, or reason
870
- * `network` for a connection that failed, whose `committed` is `'unknown'`.
871
- *
872
- * `once` sends the request exactly once, to the node in use (the confirmed
873
- * attempt of {@link callConfirmed}, whose challenge is single-use).
874
- */
875
- private retried;
876
766
  post<T = Record<string, unknown>>(path: string, body?: unknown): Promise<T>;
877
767
  get<T = Record<string, unknown>>(path: string): Promise<T>;
878
768
  put<T = Record<string, unknown>>(path: string, body?: unknown): Promise<T>;
@@ -917,7 +807,6 @@ export declare class CalimeroClient {
917
807
  * same token again (which would revoke the session).
918
808
  */
919
809
  refresh(): Promise<CalimeroSession>;
920
- private exchangeRefresh;
921
810
  /**
922
811
  * End this session. The client is signed out whether or not the node
923
812
  * answers: the revoke (`POST /auth/logout`) is attempted, and its failure
@@ -1140,8 +1029,10 @@ export declare class CalimeroClient {
1140
1029
  }, passkey?: PasskeyProvider): Promise<MoveAppResult>;
1141
1030
  /**
1142
1031
  * Move an app to another release of its package (`{ version }`), another
1143
- * package, or a bundle hash, running its migration. Answers the upgrade's
1144
- * entry; a failed migration rejects with code `UPGRADE_FAILED` (422).
1032
+ * package, or a bundle hash. An upgrade is code only: the app moves to any
1033
+ * newer release in one step. Answers the upgrade's entry; one the app's
1034
+ * group refuses rejects with code `UPGRADE_FAILED` (422), and one a
1035
+ * replica's node is too old to run with {@link RuntimeTooOld} (409).
1145
1036
  * Where owners hold app upgrades, the node asks for the approval of
1146
1037
  * whoever the package's manifest names (`412 CONFIRM_REQUIRED`), which
1147
1038
  * `passkey` gives. Refused (`403`) to a session opened on an app's
@@ -1175,6 +1066,10 @@ export declare class CalimeroClient {
1175
1066
  * confirmed retry carries `confirm` beside it.
1176
1067
  */
1177
1068
  deleteConfirmed<T>(path: string, passkey: PasskeyProvider, body?: Record<string, unknown>): Promise<T>;
1069
+ /**
1070
+ * `body` to `path`, answering a `412 CONFIRM_REQUIRED` with `passkey` when one
1071
+ * is given; `signal` aborts only the passkey prompt.
1072
+ */
1178
1073
  private confirmed;
1179
1074
  /**
1180
1075
  * Create an app instance from a bundle hash, or from a package release
@@ -1217,8 +1112,6 @@ export declare class CalimeroClient {
1217
1112
  }): Promise<ViewResult>;
1218
1113
  /** Committed events from index `from`, then live, until `signal` aborts. */
1219
1114
  events(app: string, from?: number, signal?: AbortSignal): AsyncGenerator<CloudEvent>;
1220
- /** A server-sent event stream on `path`, sealed like every other answer. */
1221
- private stream;
1222
1115
  /**
1223
1116
  * Publish this device's presence in `app`: `state` (at most 4 KiB) or `null`
1224
1117
  * to retract it. The newest `seq` wins; send the same `seq` and state again