@calimero-network/mero-js 19.13.1 → 19.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { HttpClient } from '../http-client/index.js';
2
- import type { HealthStatus, AdminAuthStatus, InstallApplicationRequest, InstallApplicationResponseData, InstallDevApplicationRequest, UninstallApplicationResponseData, ListApplicationsResponseData, GetApplicationResponseData, ApplicationVersionEntry, GetApplicationAbiResponseData, GetLatestVersionResponseData, ListPackagesResponseData, ListVersionsResponseData, CreateContextRequest, CreateContextResponseData, DeleteContextResponseData, GetContextsResponseData, Context, GenerateContextIdentityResponseData, GetContextIdentitiesResponseData, JoinContextResponseData, JoinSubgroupInheritanceResponseData, ContextGroupResponseData, ContextStorageResponseData, InviteSpecializedNodeRequest, InviteSpecializedNodeResponseData, UpdateContextApplicationRequest, ResyncContextRequest, ResyncContextResponseData, ContextsWithExecutorsResponseData, UploadBlobRequest, UploadBlobResponseData, DeleteBlobResponseData, ListBlobsResponseData, GetBlobInfoResponseData, BlobReadOptions, CreateContextAliasRequest, CreateApplicationAliasRequest, CreateDeviceAliasRequest, CreateAliasResponseData, LookupAliasResponseData, DeleteAliasResponseData, ListAliasesResponseData, ListNamespacesResponseData, CreateNamespaceRequest, CreateNamespaceResponseData, DeleteNamespaceResponseData, CreateNamespaceInvitationRequest, CreateNamespaceInvitationResponseData, CreateRecursiveInvitationResponseData, JoinNamespaceRequest, JoinNamespaceResponseData, CreateGroupInNamespaceRequest, CreateGroupInNamespaceResponseData, SubgroupEntry, Namespace, NamespaceIdentity, NodeIdentity, AccountPairInitRequest, AccountPairInitResponseData, AccountPairCompleteRequest, AccountPairCompleteResponseData, RelinkDeviceRequest, RelinkDeviceResponseData, RescopeDeviceRequest, RescopeDeviceResponseData, LabelDeviceRequest, LabelDeviceResponseData, AccountDeviceEntry, AccountApplicationEntry, RevokeAccountDeviceRequest, RevokeAccountDeviceResponseData, GroupInfoResponseData, CreateGroupRequest, CreateGroupResponseData, DeleteGroupResponseData, ListGroupMembersResponseData, ListGroupContextsResponseData, AddGroupMembersRequest, RemoveGroupMembersRequest, UpdateMemberRoleRequest, MemberCapabilities, SetMemberCapabilitiesRequest, SetDefaultCapabilitiesRequest, SetMemberAutoFollowRequest, SetSubgroupVisibilityRequest, SetTeeAdmissionPolicyRequest, GetTeeAdmissionPolicyResponseData, SetGroupMetadataRequest, SetMemberMetadataRequest, SetContextMetadataRequest, MetadataRecord, SyncGroupResponseData, UpgradeGroupRequest, UpgradeGroupResponseData, GroupUpgradeStatusResponseData, MigrationStatus, CascadeStatusEntry, RetryGroupUpgradeResponseData, ReparentGroupRequest, ReparentGroupResponseData, IssueOwnershipProofRequest, IssueNamespaceOwnershipProofRequest, IssueOwnershipProofResponseData, CreateGroupInvitationRequest, CreateGroupInvitationResponseData, CreateRecursiveGroupInvitationResponseData, JoinGroupRequest, JoinGroupResponseData, TeeInfoResponseData, TeeAttestRequest, TeeAttestResponseData, TeeVerifyQuoteRequest, TeeVerifyQuoteResponseData, PerformIntentRequest, PerformIntentResponseData, IntentRelayInfo, AdmitJoinRequest, AdmitJoinResponseData } from './admin-types.js';
2
+ import type { HealthStatus, AdminAuthStatus, InstallApplicationRequest, InstallApplicationResponseData, InstallDevApplicationRequest, UninstallApplicationResponseData, ListApplicationsResponseData, GetApplicationResponseData, ApplicationVersionEntry, GetApplicationAbiResponseData, GetLatestVersionResponseData, ListPackagesResponseData, ListVersionsResponseData, CreateContextRequest, CreateContextResponseData, DeleteContextResponseData, GetContextsResponseData, Context, GenerateContextIdentityResponseData, GetContextIdentitiesResponseData, JoinContextResponseData, JoinSubgroupInheritanceResponseData, ContextGroupResponseData, ContextStorageResponseData, InviteSpecializedNodeRequest, InviteSpecializedNodeResponseData, UpdateContextApplicationRequest, ResyncContextRequest, ResyncContextResponseData, ContextsWithExecutorsResponseData, UploadBlobRequest, UploadBlobResponseData, DeleteBlobResponseData, ListBlobsResponseData, GetBlobInfoResponseData, BlobReadOptions, CreateContextAliasRequest, CreateApplicationAliasRequest, CreateDeviceAliasRequest, CreateAliasResponseData, LookupAliasResponseData, DeleteAliasResponseData, ListAliasesResponseData, ListNamespacesResponseData, CreateNamespaceRequest, CreateNamespaceResponseData, DeleteNamespaceResponseData, CreateNamespaceInvitationRequest, CreateNamespaceInvitationResponseData, CreateRecursiveInvitationResponseData, JoinNamespaceRequest, JoinNamespaceResponseData, CreateGroupInNamespaceRequest, CreateGroupInNamespaceResponseData, SubgroupEntry, Namespace, NamespaceIdentity, NodeIdentity, AccountPairInitRequest, AccountPairInitResponseData, AccountPairCompleteRequest, AccountPairCompleteResponseData, AccountSignWithRootRequest, AccountSignWithRootResponseData, RelinkDeviceRequest, RelinkDeviceResponseData, RescopeDeviceRequest, RescopeDeviceResponseData, LabelDeviceRequest, LabelDeviceResponseData, AccountDeviceEntry, AccountApplicationEntry, RevokeAccountDeviceRequest, RevokeAccountDeviceResponseData, GroupInfoResponseData, CreateGroupRequest, CreateGroupResponseData, DeleteGroupResponseData, ListGroupMembersResponseData, ListGroupContextsResponseData, AddGroupMembersRequest, RemoveGroupMembersRequest, UpdateMemberRoleRequest, MemberCapabilities, SetMemberCapabilitiesRequest, SetDefaultCapabilitiesRequest, SetMemberAutoFollowRequest, SetSubgroupVisibilityRequest, SetTeeAdmissionPolicyRequest, GetTeeAdmissionPolicyResponseData, SetGroupMetadataRequest, SetMemberMetadataRequest, SetContextMetadataRequest, MetadataRecord, SyncGroupResponseData, UpgradeGroupRequest, UpgradeGroupResponseData, GroupUpgradeStatusResponseData, MigrationStatus, CascadeStatusEntry, RetryGroupUpgradeResponseData, ReparentGroupRequest, ReparentGroupResponseData, IssueOwnershipProofRequest, IssueNamespaceOwnershipProofRequest, IssueOwnershipProofResponseData, CreateGroupInvitationRequest, CreateGroupInvitationResponseData, CreateRecursiveGroupInvitationResponseData, JoinGroupRequest, JoinGroupResponseData, TeeInfoResponseData, TeeAttestRequest, TeeAttestResponseData, TeeVerifyQuoteRequest, TeeVerifyQuoteResponseData, PerformIntentRequest, PerformIntentResponseData, IntentRelayInfo, AdmitJoinRequest, AdmitJoinResponseData } from './admin-types.js';
3
3
  /**
4
4
  * Compare two version strings by SemVer 2.0 precedence, ascending: negative if
5
5
  * `a < b`, positive if `a > b`, `0` if equal. Build metadata (`+...`) is stripped
@@ -280,6 +280,36 @@ export declare class AdminApiClient {
280
280
  * device need not be online.
281
281
  */
282
282
  rescopeAccountDevice(deviceId: string, request: RescopeDeviceRequest): Promise<RescopeDeviceResponseData>;
283
+ /**
284
+ * Sign a payload an outside verifier specified, with this account's root.
285
+ *
286
+ * The counterpart to a verifier that issued a challenge and will check the
287
+ * signature against the account id it derives — the cloud's account link and
288
+ * login exchanges being the ones this exists for. The root never leaves the
289
+ * node, so this is the only way a client can produce that signature.
290
+ *
291
+ * `domain` is a NAME from a closed set the node knows, never the domain bytes.
292
+ * That is the security property rather than an encoding preference: signing
293
+ * caller-supplied bytes under a caller-supplied prefix would be an oracle over
294
+ * the one key that can certify a device. A node refuses a name it does not
295
+ * know, and refuses one whose bytes could be extended into a credential of its
296
+ * own.
297
+ *
298
+ * What comes back proves possession of the root and nothing else. Whether the
299
+ * payload is a challenge anyone issued, whether it is fresh, and whether the
300
+ * holder may link this account are the verifier's to decide — it issued the
301
+ * challenge and knows who to.
302
+ *
303
+ * ```ts
304
+ * const { nonce } = await cloud.getAccountLoginChallenge();
305
+ * const { rootPublicKey, signature } = await admin.signWithAccountRoot({
306
+ * domain: 'mdma.account-login',
307
+ * payload: hexEncodeUtf8(nonce),
308
+ * });
309
+ * await cloud.submitAccountLogin({ rootPublicKey, nonce, signature });
310
+ * ```
311
+ */
312
+ signWithAccountRoot(request: AccountSignWithRootRequest): Promise<AccountSignWithRootResponseData>;
283
313
  /**
284
314
  * Name a device of this account, for a listing to render.
285
315
  *
@@ -597,6 +597,38 @@ export class AdminApiClient {
597
597
  async rescopeAccountDevice(deviceId, request) {
598
598
  return unwrap(await this.httpClient.put(`/admin-api/account/devices/${encodeURIComponent(deviceId)}/scope`, request));
599
599
  }
600
+ /**
601
+ * Sign a payload an outside verifier specified, with this account's root.
602
+ *
603
+ * The counterpart to a verifier that issued a challenge and will check the
604
+ * signature against the account id it derives — the cloud's account link and
605
+ * login exchanges being the ones this exists for. The root never leaves the
606
+ * node, so this is the only way a client can produce that signature.
607
+ *
608
+ * `domain` is a NAME from a closed set the node knows, never the domain bytes.
609
+ * That is the security property rather than an encoding preference: signing
610
+ * caller-supplied bytes under a caller-supplied prefix would be an oracle over
611
+ * the one key that can certify a device. A node refuses a name it does not
612
+ * know, and refuses one whose bytes could be extended into a credential of its
613
+ * own.
614
+ *
615
+ * What comes back proves possession of the root and nothing else. Whether the
616
+ * payload is a challenge anyone issued, whether it is fresh, and whether the
617
+ * holder may link this account are the verifier's to decide — it issued the
618
+ * challenge and knows who to.
619
+ *
620
+ * ```ts
621
+ * const { nonce } = await cloud.getAccountLoginChallenge();
622
+ * const { rootPublicKey, signature } = await admin.signWithAccountRoot({
623
+ * domain: 'mdma.account-login',
624
+ * payload: hexEncodeUtf8(nonce),
625
+ * });
626
+ * await cloud.submitAccountLogin({ rootPublicKey, nonce, signature });
627
+ * ```
628
+ */
629
+ async signWithAccountRoot(request) {
630
+ return unwrap(await this.httpClient.post('/admin-api/account/sign-with-root', request));
631
+ }
600
632
  /**
601
633
  * Name a device of this account, for a listing to render.
602
634
  *
@@ -800,6 +800,61 @@ export interface AccountPairCompleteResponseData {
800
800
  */
801
801
  credential: string;
802
802
  }
803
+ /**
804
+ * A domain an outside verifier defined, which the account root may sign under.
805
+ *
806
+ * A name from a closed set, never the domain bytes. The node prepends the bytes
807
+ * itself: "sign these bytes with the account root" would be a signing oracle
808
+ * over the one key that can certify a device, which is account takeover. A node
809
+ * refuses any name it does not know.
810
+ *
811
+ * Typed as a union plus `(string & {})` so a node newer than this SDK can be
812
+ * reached without a cast, while the known names still autocomplete.
813
+ */
814
+ export type ExternalSigningDomain = 'mdma.account-link' | 'mdma.account-login' | 'mdma.account-recovery' | (string & {});
815
+ /**
816
+ * Ask this node's account root to sign a payload an outside verifier specified.
817
+ *
818
+ * The verifier owns the format; the node supplies only the domain. For the cloud
819
+ * this is how a challenge it sealed and issued gets signed by a root that
820
+ * deliberately never leaves the node holding it.
821
+ */
822
+ export interface AccountSignWithRootRequest {
823
+ /** Which verifier's domain to sign under. */
824
+ domain: ExternalSigningDomain;
825
+ /**
826
+ * The bytes to sign after the domain, hex-encoded.
827
+ *
828
+ * Hex rather than text because the field is bytes. For the cloud's challenge —
829
+ * an ASCII string — that is the UTF-8 of it, hex-encoded; {@link hexEncodeUtf8}
830
+ * does exactly that and exists so each caller does not re-derive it.
831
+ */
832
+ payload: string;
833
+ }
834
+ /**
835
+ * Hex-encode a string's UTF-8 bytes, for {@link AccountSignWithRootRequest.payload}.
836
+ *
837
+ * Exists so each caller does not write the same loop. Every verifier this is
838
+ * used with issues its challenge as text, and the node takes bytes — getting
839
+ * that conversion subtly wrong (UTF-16 code units, a stray newline from a shell)
840
+ * produces a signature over the wrong message, which fails at the verifier with
841
+ * nothing local to point at.
842
+ */
843
+ export declare function hexEncodeUtf8(text: string): string;
844
+ /** What the node signed, and the key to check it against. */
845
+ export interface AccountSignWithRootResponseData {
846
+ /** The account root's public key, 64 hex characters. */
847
+ rootPublicKey: string;
848
+ /**
849
+ * The signature over `domain ‖ payload`, **base64**.
850
+ *
851
+ * Base64 rather than hex because that is what the consuming verifiers decode;
852
+ * re-encoding it on the way out is a step that fails silently at the far end.
853
+ */
854
+ signature: string;
855
+ /** The account the signing key belongs to, 64 hex characters. */
856
+ accountId: string;
857
+ }
803
858
  /**
804
859
  * Repair or widen the reach of a device this account already certified.
805
860
  *
@@ -1,3 +1,14 @@
1
1
  // Admin API Types — aligned with core server routes
2
2
  // All types use camelCase to match core's #[serde(rename_all = "camelCase")]
3
- export {};
3
+ /**
4
+ * Hex-encode a string's UTF-8 bytes, for {@link AccountSignWithRootRequest.payload}.
5
+ *
6
+ * Exists so each caller does not write the same loop. Every verifier this is
7
+ * used with issues its challenge as text, and the node takes bytes — getting
8
+ * that conversion subtly wrong (UTF-16 code units, a stray newline from a shell)
9
+ * produces a signature over the wrong message, which fails at the verifier with
10
+ * nothing local to point at.
11
+ */
12
+ export function hexEncodeUtf8(text) {
13
+ return Array.from(new TextEncoder().encode(text), (b) => b.toString(16).padStart(2, '0')).join('');
14
+ }
@@ -6,6 +6,32 @@ import { AuthRevokedError, HTTPError } from '../http-client/index.js';
6
6
  * all — is recoverable by refreshing and reconnecting.
7
7
  */
8
8
  const TERMINAL_AUTH_ERRORS = new Set(['token_reuse', 'token_revoked']);
9
+ /**
10
+ * Statuses a terminal `x-auth-error` can arrive on.
11
+ *
12
+ * Measured against merod 0.11.0-rc.41: a revoked token family answers
13
+ * `GET /sse` and `POST /sse/subscription` with **403**, `x-auth-error:
14
+ * token_revoked` and `content-length: 0` — `token_expired` is the 401.
15
+ * Keying the check on 401 alone made the branch below unreachable for the one
16
+ * reason a stream actually sees, so a dead token family surfaced as a bare
17
+ * `HTTPError(403)` and reconnected on a timer forever.
18
+ */
19
+ const TERMINAL_AUTH_STATUSES = new Set([401, 403]);
20
+ /**
21
+ * Whether a failed response means the whole token family is gone, so the caller
22
+ * must re-authenticate and the client must stop reconnecting.
23
+ *
24
+ * The body carries nothing on this path (`content-length: 0`), so the header is
25
+ * the only evidence there is.
26
+ */
27
+ function isTerminalAuthFailure(response) {
28
+ const authError = response.headers.get('x-auth-error');
29
+ if (!authError)
30
+ return null;
31
+ if (!TERMINAL_AUTH_STATUSES.has(response.status))
32
+ return null;
33
+ return TERMINAL_AUTH_ERRORS.has(authError) ? authError : null;
34
+ }
9
35
  export class SseClient {
10
36
  constructor(opts) {
11
37
  this.sessionId = null;
@@ -110,12 +136,10 @@ export class SseClient {
110
136
  // 401 as fatal — logs the user out on a routine reconnect after the
111
137
  // access token ages out, discarding a refresh token that was still
112
138
  // good. The distinction is in the header; surface it.
113
- const authError = response.headers.get('x-auth-error');
139
+ const terminal = isTerminalAuthFailure(response);
114
140
  const bodyText = await response.text().catch(() => undefined);
115
- if (response.status === 401 &&
116
- authError &&
117
- TERMINAL_AUTH_ERRORS.has(authError)) {
118
- throw new AuthRevokedError(authError, response.status, response.statusText, url, response.headers, bodyText);
141
+ if (terminal) {
142
+ throw new AuthRevokedError(terminal, response.status, response.statusText, url, response.headers, bodyText);
119
143
  }
120
144
  throw new HTTPError(response.status, response.statusText, url, response.headers, bodyText);
121
145
  }
@@ -135,6 +159,11 @@ export class SseClient {
135
159
  return;
136
160
  const error = err instanceof Error ? err : new Error(String(err));
137
161
  this.emit('error', error);
162
+ // Reconnecting against a revoked token family is a timer that never
163
+ // succeeds: every attempt re-sends the same dead credential. The caller
164
+ // has to re-authenticate, and an AuthRevokedError is how it is told.
165
+ if (error instanceof AuthRevokedError)
166
+ return;
138
167
  this.scheduleReconnect();
139
168
  }
140
169
  }
@@ -284,7 +313,17 @@ export class SseClient {
284
313
  }),
285
314
  });
286
315
  if (!response.ok) {
287
- this.emit('error', new Error(`SSE ${method} failed: ${response.status}`));
316
+ // The subscribe path used to report only the status. On the one failure
317
+ // that matters it is the whole of what the caller got: a revoked family
318
+ // answers 403 with an empty body, so a bare `SSE subscribe failed: 403`
319
+ // discarded the only evidence of what went wrong — and a 403 here reads
320
+ // identically to the unrelated "not the session owner" refusal, which
321
+ // does carry a body.
322
+ const terminal = isTerminalAuthFailure(response);
323
+ const bodyText = await response.text().catch(() => undefined);
324
+ this.emit('error', terminal
325
+ ? new AuthRevokedError(terminal, response.status, response.statusText, `${this.baseUrl}/sse/subscription`, response.headers, bodyText)
326
+ : new HTTPError(response.status, response.statusText, `${this.baseUrl}/sse/subscription`, response.headers, bodyText));
288
327
  }
289
328
  }
290
329
  catch (err) {