@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.
- package/dist/admin-api/admin-client.d.ts +31 -1
- package/dist/admin-api/admin-client.js +32 -0
- package/dist/admin-api/admin-types.d.ts +55 -0
- package/dist/admin-api/admin-types.js +12 -1
- package/dist/events/sse.js +45 -6
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +76 -4
- package/dist/index.cjs.map +3 -3
- package/dist/index.mjs +76 -4
- package/dist/index.mjs.map +3 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/events/sse.js
CHANGED
|
@@ -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
|
|
139
|
+
const terminal = isTerminalAuthFailure(response);
|
|
114
140
|
const bodyText = await response.text().catch(() => undefined);
|
|
115
|
-
if (
|
|
116
|
-
|
|
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
|
-
|
|
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) {
|