@calimero-network/mero-js 25.3.3 → 26.0.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/account/join-with-node.d.ts +7 -12
- package/dist/account/join-with-node.js +7 -12
- package/dist/account/session.d.ts +3 -6
- package/dist/account/session.js +3 -6
- package/dist/admin-api/admin-client.d.ts +2 -133
- package/dist/admin-api/admin-client.js +1 -226
- package/dist/admin-api/admin-types.d.ts +0 -146
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +3 -3
- package/dist/index.cjs +0 -262
- package/dist/index.cjs.map +2 -2
- package/dist/index.mjs +0 -262
- package/dist/index.mjs.map +2 -2
- package/dist/relay/index.d.ts +2 -2
- package/dist/relay/index.js +1 -1
- package/dist/relay/nonce-source.d.ts +4 -127
- package/dist/relay/nonce-source.js +2 -154
- package/dist/relay/relay-client.d.ts +3 -3
- package/package.json +1 -1
|
@@ -21,18 +21,13 @@
|
|
|
21
21
|
* `signer == credential.statement.sign_pk` before applying a join — so an
|
|
22
22
|
* admitter can carry this and can never author it: it cannot admit a different
|
|
23
23
|
* account, change the group, or grant a role. It can refuse, and that is all.
|
|
24
|
-
* This is also why the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* shape that is actually useful to a caller is the one that hands back a
|
|
32
|
-
* {@link DelegatedSession} — the node that admitted you is the node to connect
|
|
33
|
-
* through — which is `mero-react`'s concern. The pieces that are pure SDK, the
|
|
34
|
-
* borsh op layout and the request type, are used from `mero-js` unchanged and
|
|
35
|
-
* are not reimplemented here.
|
|
24
|
+
* This is also why core serves the route without a credential (it sits on the
|
|
25
|
+
* public admin router, beside `/health` and the intent routes, on every node
|
|
26
|
+
* whatever its auth mode), why the fleet ingress exempts the one anchored path,
|
|
27
|
+
* and why this posts with `fetch` rather than through `AdminApiClient`, which is
|
|
28
|
+
* built around a node credential this caller does not have. A node that predates
|
|
29
|
+
* that change and runs embedded auth answers 401 here; the failure is reported at
|
|
30
|
+
* the `admit` step like any other refusal.
|
|
36
31
|
*
|
|
37
32
|
* ## Why the op has no parents
|
|
38
33
|
*
|
|
@@ -21,18 +21,13 @@
|
|
|
21
21
|
* `signer == credential.statement.sign_pk` before applying a join — so an
|
|
22
22
|
* admitter can carry this and can never author it: it cannot admit a different
|
|
23
23
|
* account, change the group, or grant a role. It can refuse, and that is all.
|
|
24
|
-
* This is also why the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* shape that is actually useful to a caller is the one that hands back a
|
|
32
|
-
* {@link DelegatedSession} — the node that admitted you is the node to connect
|
|
33
|
-
* through — which is `mero-react`'s concern. The pieces that are pure SDK, the
|
|
34
|
-
* borsh op layout and the request type, are used from `mero-js` unchanged and
|
|
35
|
-
* are not reimplemented here.
|
|
24
|
+
* This is also why core serves the route without a credential (it sits on the
|
|
25
|
+
* public admin router, beside `/health` and the intent routes, on every node
|
|
26
|
+
* whatever its auth mode), why the fleet ingress exempts the one anchored path,
|
|
27
|
+
* and why this posts with `fetch` rather than through `AdminApiClient`, which is
|
|
28
|
+
* built around a node credential this caller does not have. A node that predates
|
|
29
|
+
* that change and runs embedded auth answers 401 here; the failure is reported at
|
|
30
|
+
* the `admit` step like any other refusal.
|
|
36
31
|
*
|
|
37
32
|
* ## Why the op has no parents
|
|
38
33
|
*
|
|
@@ -77,12 +77,9 @@ export declare function clearDelegatedCredential(): void;
|
|
|
77
77
|
* replay a spent nonce and be refused. Keyed by relay and context because the
|
|
78
78
|
* ledger the node keeps is per `(context, author device)`.
|
|
79
79
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* delegated client on a relay cannot reach it. Until it moves, holding the
|
|
84
|
-
* counter locally is the only option, and losing storage means burning refusals
|
|
85
|
-
* until the counter catches up.
|
|
80
|
+
* No node serves a "where does my sequence stand" lookup (core #4018), so
|
|
81
|
+
* holding the counter locally is the only option, and losing storage means
|
|
82
|
+
* burning refusals until the counter catches up.
|
|
86
83
|
*/
|
|
87
84
|
export declare function persistedNonces(relayUrl: string): {
|
|
88
85
|
next: () => Promise<bigint>;
|
package/dist/account/session.js
CHANGED
|
@@ -116,12 +116,9 @@ export function clearDelegatedCredential() {
|
|
|
116
116
|
* replay a spent nonce and be refused. Keyed by relay and context because the
|
|
117
117
|
* ledger the node keeps is per `(context, author device)`.
|
|
118
118
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* delegated client on a relay cannot reach it. Until it moves, holding the
|
|
123
|
-
* counter locally is the only option, and losing storage means burning refusals
|
|
124
|
-
* until the counter catches up.
|
|
119
|
+
* No node serves a "where does my sequence stand" lookup (core #4018), so
|
|
120
|
+
* holding the counter locally is the only option, and losing storage means
|
|
121
|
+
* burning refusals until the counter catches up.
|
|
125
122
|
*/
|
|
126
123
|
export function persistedNonces(relayUrl) {
|
|
127
124
|
// ONE sequence per relay, not one per context. A client is rebuilt whenever
|
|
@@ -1,37 +1,6 @@
|
|
|
1
|
-
import { HttpClient
|
|
2
|
-
import type { HealthStatus, ReadinessStatus, AdminAuthStatus, InstallApplicationRequest, InstallApplicationResponseData, InstallDevApplicationRequest, UninstallApplicationResponseData, ListApplicationsResponseData, GetApplicationResponseData, ApplicationVersionEntry, GetApplicationAbiResponseData, GetLatestVersionResponseData, ListPackagesResponseData, ListVersionsResponseData, CreateContextRequest, CreateContextResponseData, DeleteContextResponseData, GetContextsResponseData, Context, GenerateContextIdentityResponseData, GetContextIdentitiesResponseData, JoinContextResponseData, JoinSubgroupInheritanceResponseData, ContextGroupResponseData, ContextStorageResponseData,
|
|
1
|
+
import { HttpClient } from '../http-client/index.js';
|
|
2
|
+
import type { HealthStatus, ReadinessStatus, AdminAuthStatus, InstallApplicationRequest, InstallApplicationResponseData, InstallDevApplicationRequest, UninstallApplicationResponseData, ListApplicationsResponseData, GetApplicationResponseData, ApplicationVersionEntry, GetApplicationAbiResponseData, GetLatestVersionResponseData, ListPackagesResponseData, ListVersionsResponseData, CreateContextRequest, CreateContextResponseData, DeleteContextResponseData, GetContextsResponseData, Context, GenerateContextIdentityResponseData, GetContextIdentitiesResponseData, JoinContextResponseData, JoinSubgroupInheritanceResponseData, ContextGroupResponseData, ContextStorageResponseData, 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, NodeIdentity, AccountPairInitRequest, AccountPairInitResponseData, AccountPairCompleteRequest, AccountPairCompleteResponseData, AccountSignWithRootRequest, AccountSignWithRootResponseData, RelinkDeviceRequest, RelinkDeviceResponseData, RescopeDeviceRequest, RescopeDeviceResponseData, LabelDeviceRequest, LabelDeviceResponseData, AccountDeviceEntry, AccountApplicationEntry, RevokeAccountDeviceRequest, LinkAccountDeviceRequest, LinkAccountDeviceResponseData, RevokeAccountDeviceResponseData, MemberDevicesEntry, ListMemberDevicesOptions, GroupInfoResponseData, CreateGroupRequest, CreateGroupResponseData, DeleteGroupResponseData, ListGroupMembersResponseData, ListGroupContextsResponseData, AddGroupMembersRequest, RemoveGroupMembersRequest, UpdateMemberRoleRequest, MemberCapabilities, SetMemberCapabilitiesRequest, SetDefaultCapabilitiesRequest, SetMemberAutoFollowRequest, SetSubgroupVisibilityRequest, SetTeeAdmissionPolicyRequest, GetTeeAdmissionPolicyResponseData, SetTeeAuthoringPolicyRequest, TransferOwnershipRequest, ChangeNamespaceAdminRequest, OwnerDeleteGroupRequest, RootGuardedOpRequest, SetGroupMetadataRequest, SetMemberMetadataRequest, SetContextMetadataRequest, MetadataRecord, SyncGroupResponseData, UpgradeGroupRequest, UpgradeGroupResponseData, GroupUpgradeStatusResponseData, MigrationStatus, NamespaceRevokedDevices, CascadeStatusEntry, RetryGroupUpgradeResponseData, ReparentGroupRequest, ReparentGroupResponseData, IssueOwnershipProofRequest, IssueNamespaceOwnershipProofRequest, IssueOwnershipProofResponseData, CreateGroupInvitationRequest, CreateGroupInvitationResponseData, CreateRecursiveGroupInvitationResponseData, JoinGroupRequest, JoinGroupResponseData, TeeInfoResponseData, TeeAttestRequest, TeeAttestResponseData, PerformIntentRequest, PerformIntentResponseData, IntentRelayInfo, QueryContextRequest, QueryContextResponseData, ListMemberDevicesResponseData, SealToAccountRequest, SealedEnvelope, AdmitJoinRequest, AdmitJoinResponseData } from './admin-types.js';
|
|
3
3
|
import { type RedeemOutcome } from './redeem-invitation.js';
|
|
4
|
-
/**
|
|
5
|
-
* Thrown when a node answers the warrant-nonce route with a 404.
|
|
6
|
-
*
|
|
7
|
-
* Its own type because the meaning is unambiguous and not "not found": the
|
|
8
|
-
* handler never 404s — an unknown device is a `200` with `seen: false`, a
|
|
9
|
-
* malformed id is a `400` — so a 404 says the route is not mounted on this
|
|
10
|
-
* node. It is new on core and absent from `master` builds. A caller that sees
|
|
11
|
-
* this should fall back to its own counter (or refuse to write) rather than
|
|
12
|
-
* retry, because no amount of retrying adds a route to a running node.
|
|
13
|
-
*/
|
|
14
|
-
export declare class WarrantNonceRouteUnavailableError extends Error {
|
|
15
|
-
/** The path that answered 404. */
|
|
16
|
-
readonly path: string;
|
|
17
|
-
/** The underlying HTTP failure, kept for logs. */
|
|
18
|
-
readonly cause: HTTPError;
|
|
19
|
-
name: string;
|
|
20
|
-
constructor(
|
|
21
|
-
/** The path that answered 404. */
|
|
22
|
-
path: string,
|
|
23
|
-
/** The underlying HTTP failure, kept for logs. */
|
|
24
|
-
cause: HTTPError);
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Shape the node's answer into the union, which is where the exhausted case
|
|
28
|
-
* stops being a missing field and starts being something the compiler asks
|
|
29
|
-
* about.
|
|
30
|
-
*
|
|
31
|
-
* Exported for tests: it is the whole client-side contract of the route, and
|
|
32
|
-
* testing it needs no transport.
|
|
33
|
-
*/
|
|
34
|
-
export declare function parseWarrantNonce(body: string): WarrantNonceState;
|
|
35
4
|
/**
|
|
36
5
|
* Compare two version strings by SemVer 2.0 precedence, ascending: negative if
|
|
37
6
|
* `a < b`, positive if `a > b`, `0` if equal. Build metadata (`+...`) is stripped
|
|
@@ -108,7 +77,6 @@ export declare class AdminApiClient {
|
|
|
108
77
|
* context as stranded.
|
|
109
78
|
*/
|
|
110
79
|
resyncContext(contextId: string, request?: ResyncContextRequest): Promise<ResyncContextResponseData>;
|
|
111
|
-
inviteSpecializedNode(request: InviteSpecializedNodeRequest): Promise<InviteSpecializedNodeResponseData>;
|
|
112
80
|
updateContextApplication(contextId: string, request: UpdateContextApplicationRequest): Promise<void>;
|
|
113
81
|
getContextsWithExecutorsForApplication(applicationId: string): Promise<ContextsWithExecutorsResponseData>;
|
|
114
82
|
uploadBlob(request: UploadBlobRequest): Promise<UploadBlobResponseData>;
|
|
@@ -169,26 +137,6 @@ export declare class AdminApiClient {
|
|
|
169
137
|
* namespace.
|
|
170
138
|
*/
|
|
171
139
|
getNodeIdentity(): Promise<NodeIdentity>;
|
|
172
|
-
/**
|
|
173
|
-
* @deprecated Use {@link getNodeIdentity} instead. Every field this returns is
|
|
174
|
-
* node-level, so the namespace was only ever decoration — it now delegates to
|
|
175
|
-
* the node-level route and reshapes the answer.
|
|
176
|
-
*
|
|
177
|
-
* Three consequences of that delegation, all visible to a caller who relied on
|
|
178
|
-
* the old route's behaviour:
|
|
179
|
-
*
|
|
180
|
-
* - **No participation gate.** The namespace route answered 404 when this node
|
|
181
|
-
* had never joined the namespace; this answers whatever the node's identity
|
|
182
|
-
* is, regardless. If you were using it to ask "am I in this namespace?", ask
|
|
183
|
-
* {@link listNamespaces} instead — that is the question you were really
|
|
184
|
-
* asking.
|
|
185
|
-
* - **`namespaceId` is echoed verbatim.** The old route resolved a subgroup id
|
|
186
|
-
* to its root namespace and returned that; passing a subgroup id here gives
|
|
187
|
-
* the subgroup id back.
|
|
188
|
-
* - **404 means something else.** Previously "not my namespace", now "this node
|
|
189
|
-
* holds no account root yet".
|
|
190
|
-
*/
|
|
191
|
-
getNamespaceIdentity(namespaceId: string): Promise<NamespaceIdentity>;
|
|
192
140
|
/**
|
|
193
141
|
* Ask this node to run one method on a member's behalf, under a warrant that
|
|
194
142
|
* member signed.
|
|
@@ -486,80 +434,6 @@ export declare class AdminApiClient {
|
|
|
486
434
|
* same reason {@link listAccountDevices} unwraps `devices`.
|
|
487
435
|
*/
|
|
488
436
|
listGroupMemberDevices(groupId: string, options?: ListMemberDevicesOptions): Promise<MemberDevicesEntry[]>;
|
|
489
|
-
/**
|
|
490
|
-
* Where `authorDeviceKey` stands in its warrant-nonce sequence in `contextId`.
|
|
491
|
-
*
|
|
492
|
-
* # What this is for
|
|
493
|
-
*
|
|
494
|
-
* The nonce is the one input to a warrant that is stateful on the *client*.
|
|
495
|
-
* Everything else an author needs it either holds (its keys, the method, the
|
|
496
|
-
* args) or can read from the relay ({@link RelayClient.describe} gives the
|
|
497
|
-
* executor account and the grant). So an author that has lost its counter —
|
|
498
|
-
* a browser keyholder in partitioned or periodically cleared storage, which
|
|
499
|
-
* is its normal lifecycle rather than a fault — otherwise has exactly one
|
|
500
|
-
* strategy: guess upward and burn a refused round trip per wrong guess, with
|
|
501
|
-
* each refusal indistinguishable at the API from a genuinely spent nonce.
|
|
502
|
-
*
|
|
503
|
-
* Read {@link WarrantNonceOpen.nextNonce} and mint there. If you still hold
|
|
504
|
-
* your own counter, mint at `max(ownNext, nextNonce)`: nonce state folds per
|
|
505
|
-
* peer, so a node can only be BEHIND a client whose warrants it has not
|
|
506
|
-
* applied — never ahead of the truth. An author spread across several relays
|
|
507
|
-
* asks each and takes the highest answer. {@link createRecoveringNonceSource}
|
|
508
|
-
* does all of that.
|
|
509
|
-
*
|
|
510
|
-
* Purely a read. It spends nothing and advances nothing, so it may be called
|
|
511
|
-
* as often as you like.
|
|
512
|
-
*
|
|
513
|
-
* # Forward compatibility — this route is NEW
|
|
514
|
-
*
|
|
515
|
-
* It is not on core `master` yet, so a current node answers **404**, which
|
|
516
|
-
* this method surfaces as {@link WarrantNonceRouteUnavailableError} rather
|
|
517
|
-
* than as an opaque {@link HTTPError}. That distinction matters because the
|
|
518
|
-
* handler itself never 404s: a device with no row is a `200` with
|
|
519
|
-
* `seen: false`, and a malformed id is a `400`. So a 404 here means the node
|
|
520
|
-
* does not serve the route at all — an older build, or a deployment that has
|
|
521
|
-
* not re-mounted it — and the answer is to fall back to a local counter, not
|
|
522
|
-
* to retry.
|
|
523
|
-
*
|
|
524
|
-
* # Authentication
|
|
525
|
-
*
|
|
526
|
-
* On the PROTECTED admin router, unlike the two delegated-execution routes it
|
|
527
|
-
* sits beside: it takes another principal's device key as input and reports
|
|
528
|
-
* that principal's activity. A `--public-intents` deployment does not serve
|
|
529
|
-
* this anonymously, so an admin credential is required.
|
|
530
|
-
*
|
|
531
|
-
* @param authorDeviceKey the device's signing key, base58 (43-44 chars) or
|
|
532
|
-
* hex (exactly 64). The node accepts either; the two never collide.
|
|
533
|
-
*/
|
|
534
|
-
getWarrantNonce(contextId: string, authorDeviceKey: string): Promise<WarrantNonceState>;
|
|
535
|
-
/**
|
|
536
|
-
* The same answer, asked for the way a delegated client has to ask.
|
|
537
|
-
*
|
|
538
|
-
* {@link getWarrantNonce} is on the protected admin router, so a keyholder
|
|
539
|
-
* writing through a relay — the client this whole facility exists for — is
|
|
540
|
-
* refused it: measured `403` for a session carrying exactly
|
|
541
|
-
* `context:intent`, `context:query`, `context:subscribe`. That client has no
|
|
542
|
-
* admin credential and cannot be given one without defeating the point of the
|
|
543
|
-
* delegated surface.
|
|
544
|
-
*
|
|
545
|
-
* What it does have is the credential proving the device is its own, which it
|
|
546
|
-
* already sends on every write. Presenting it here makes the request
|
|
547
|
-
* self-scoped: you learn a device's position only by proving, at call time,
|
|
548
|
-
* that the device is yours. So there is no oracle to protect against, and no
|
|
549
|
-
* reason for an admin gate.
|
|
550
|
-
*
|
|
551
|
-
* The device key is NOT a parameter. It is read from the verified
|
|
552
|
-
* certificate server-side, which is the only reading that cannot disagree
|
|
553
|
-
* with the signature — a key supplied alongside would be a second spelling of
|
|
554
|
-
* the same fact, and two spellings can differ.
|
|
555
|
-
*
|
|
556
|
-
* Returns the identical shape {@link getWarrantNonce} returns, so a caller can
|
|
557
|
-
* swap one for the other without touching what it does with the answer.
|
|
558
|
-
*
|
|
559
|
-
* @param authorProof hex-encoded borsh `AccountProof<DeviceCert>` — the same
|
|
560
|
-
* value passed as `authorProof` to the relay client.
|
|
561
|
-
*/
|
|
562
|
-
getWarrantNonceAsAuthor(contextId: string, authorProof: string): Promise<WarrantNonceState>;
|
|
563
437
|
/**
|
|
564
438
|
* Member accounts in the group and the devices bound to each. An admin node
|
|
565
439
|
* sees every account; a plain member only its own. `403` if this node is not a
|
|
@@ -734,11 +608,6 @@ export declare class AdminApiClient {
|
|
|
734
608
|
joinSubgroupInheritance(groupId: string): Promise<JoinSubgroupInheritanceResponseData>;
|
|
735
609
|
getTeeInfo(): Promise<TeeInfoResponseData>;
|
|
736
610
|
teeAttest(request: TeeAttestRequest): Promise<TeeAttestResponseData>;
|
|
737
|
-
/**
|
|
738
|
-
* @deprecated Nodes no longer serve `/admin-api/tee/verify-quote` (removed in
|
|
739
|
-
* core#3262), so this fails. Verify quotes with `createQuoteVerifier`.
|
|
740
|
-
*/
|
|
741
|
-
teeVerifyQuote(request: TeeVerifyQuoteRequest): Promise<TeeVerifyQuoteResponseData>;
|
|
742
611
|
getPeersCount(): Promise<{
|
|
743
612
|
count: number;
|
|
744
613
|
}>;
|
|
@@ -1,92 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { withRetry } from '../http-client/index.js';
|
|
2
2
|
import { pathSegment } from '../http-client/path-segment.js';
|
|
3
3
|
import { CAPABILITIES, hasCap, withCap } from '../capabilities.js';
|
|
4
4
|
import { redeemInvitation } from './redeem-invitation.js';
|
|
5
|
-
/**
|
|
6
|
-
* Thrown when a node answers the warrant-nonce route with a 404.
|
|
7
|
-
*
|
|
8
|
-
* Its own type because the meaning is unambiguous and not "not found": the
|
|
9
|
-
* handler never 404s — an unknown device is a `200` with `seen: false`, a
|
|
10
|
-
* malformed id is a `400` — so a 404 says the route is not mounted on this
|
|
11
|
-
* node. It is new on core and absent from `master` builds. A caller that sees
|
|
12
|
-
* this should fall back to its own counter (or refuse to write) rather than
|
|
13
|
-
* retry, because no amount of retrying adds a route to a running node.
|
|
14
|
-
*/
|
|
15
|
-
export class WarrantNonceRouteUnavailableError extends Error {
|
|
16
|
-
constructor(
|
|
17
|
-
/** The path that answered 404. */
|
|
18
|
-
path,
|
|
19
|
-
/** The underlying HTTP failure, kept for logs. */
|
|
20
|
-
cause) {
|
|
21
|
-
super(`this node does not serve warrant-nonce discovery (404 on ${path}); it predates the route, so a client must fall back to its own nonce counter`);
|
|
22
|
-
this.path = path;
|
|
23
|
-
this.cause = cause;
|
|
24
|
-
this.name = 'WarrantNonceRouteUnavailableError';
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Recover a `u64` from the response bytes rather than from a parsed `number`.
|
|
29
|
-
*
|
|
30
|
-
* `JSON.parse` has already rounded anything past 2^53 by the time a value is
|
|
31
|
-
* handed over, and it rounds without complaint — so the digits are read back
|
|
32
|
-
* out of the source text and turned straight into a `bigint`.
|
|
33
|
-
*
|
|
34
|
-
* Matching on the quoted key is safe for exactly this response: its only two
|
|
35
|
-
* string fields are a context id and a device key, both of which are hex or
|
|
36
|
-
* base58 and so cannot contain a quote, a colon, or these field names. The
|
|
37
|
-
* parsed object still decides whether a field is *present*; the text is
|
|
38
|
-
* consulted only for its digits.
|
|
39
|
-
*/
|
|
40
|
-
function u64FromBody(body, parsed, field) {
|
|
41
|
-
const value = parsed[field];
|
|
42
|
-
if (value === undefined || value === null)
|
|
43
|
-
return undefined;
|
|
44
|
-
const digits = new RegExp(`"${field}"\\s*:\\s*(\\d+)`).exec(body);
|
|
45
|
-
if (digits)
|
|
46
|
-
return BigInt(digits[1]);
|
|
47
|
-
// The field is present but not a bare integer literal in the body — a node
|
|
48
|
-
// that started sending it as a string, say. Accept a decimal string, and
|
|
49
|
-
// refuse anything else loudly instead of coercing a nonce into existence.
|
|
50
|
-
if (typeof value === 'string' && /^\d+$/.test(value))
|
|
51
|
-
return BigInt(value);
|
|
52
|
-
throw new Error(`warrant-nonce response field \`${field}\` is not a u64: ${JSON.stringify(value)}`);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Shape the node's answer into the union, which is where the exhausted case
|
|
56
|
-
* stops being a missing field and starts being something the compiler asks
|
|
57
|
-
* about.
|
|
58
|
-
*
|
|
59
|
-
* Exported for tests: it is the whole client-side contract of the route, and
|
|
60
|
-
* testing it needs no transport.
|
|
61
|
-
*/
|
|
62
|
-
export function parseWarrantNonce(body) {
|
|
63
|
-
const parsed = JSON.parse(body);
|
|
64
|
-
const data = parsed?.data;
|
|
65
|
-
if (!data || typeof data !== 'object') {
|
|
66
|
-
throw new Error(`warrant-nonce response had no \`data\`: ${body.slice(0, 200)}`);
|
|
67
|
-
}
|
|
68
|
-
const common = {
|
|
69
|
-
contextId: String(data.contextId ?? ''),
|
|
70
|
-
authorDeviceKey: String(data.authorDeviceKey ?? ''),
|
|
71
|
-
seen: data.seen === true,
|
|
72
|
-
highWaterNonce: u64FromBody(body, data, 'highWaterNonce'),
|
|
73
|
-
// Not defaulted: a node that stopped sending it is a node whose window this
|
|
74
|
-
// client would be guessing at, and the window is not a nonce input anyway.
|
|
75
|
-
windowWidth: u64FromBody(body, data, 'windowWidth') ?? 0n,
|
|
76
|
-
};
|
|
77
|
-
const nextNonce = u64FromBody(body, data, 'nextNonce');
|
|
78
|
-
if (nextNonce === undefined) {
|
|
79
|
-
// The one absence with a meaning: `u64::MAX` is spent. The node omits the
|
|
80
|
-
// field rather than wrapping to `0`, and a client must not fill the gap.
|
|
81
|
-
return {
|
|
82
|
-
...common,
|
|
83
|
-
kind: 'exhausted',
|
|
84
|
-
seen: true,
|
|
85
|
-
highWaterNonce: common.highWaterNonce ?? 0xffffffffffffffffn,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
return { ...common, kind: 'open', nextNonce };
|
|
89
|
-
}
|
|
90
5
|
/**
|
|
91
6
|
* Helper: server wraps most responses in `{ data: T }`.
|
|
92
7
|
* This extracts `.data` so callers get the inner payload directly. Null-safe so an
|
|
@@ -334,9 +249,6 @@ export class AdminApiClient {
|
|
|
334
249
|
// callers always get a typed value instead of null.
|
|
335
250
|
return r ?? { contextId, resyncStarted: true };
|
|
336
251
|
}
|
|
337
|
-
async inviteSpecializedNode(request) {
|
|
338
|
-
return unwrap(await this.httpClient.post('/admin-api/contexts/invite-specialized-node', request));
|
|
339
|
-
}
|
|
340
252
|
async updateContextApplication(contextId, request) {
|
|
341
253
|
await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/application`, request);
|
|
342
254
|
}
|
|
@@ -495,29 +407,6 @@ export class AdminApiClient {
|
|
|
495
407
|
async getNodeIdentity() {
|
|
496
408
|
return unwrap(await this.httpClient.get('/admin-api/identity'));
|
|
497
409
|
}
|
|
498
|
-
/**
|
|
499
|
-
* @deprecated Use {@link getNodeIdentity} instead. Every field this returns is
|
|
500
|
-
* node-level, so the namespace was only ever decoration — it now delegates to
|
|
501
|
-
* the node-level route and reshapes the answer.
|
|
502
|
-
*
|
|
503
|
-
* Three consequences of that delegation, all visible to a caller who relied on
|
|
504
|
-
* the old route's behaviour:
|
|
505
|
-
*
|
|
506
|
-
* - **No participation gate.** The namespace route answered 404 when this node
|
|
507
|
-
* had never joined the namespace; this answers whatever the node's identity
|
|
508
|
-
* is, regardless. If you were using it to ask "am I in this namespace?", ask
|
|
509
|
-
* {@link listNamespaces} instead — that is the question you were really
|
|
510
|
-
* asking.
|
|
511
|
-
* - **`namespaceId` is echoed verbatim.** The old route resolved a subgroup id
|
|
512
|
-
* to its root namespace and returned that; passing a subgroup id here gives
|
|
513
|
-
* the subgroup id back.
|
|
514
|
-
* - **404 means something else.** Previously "not my namespace", now "this node
|
|
515
|
-
* holds no account root yet".
|
|
516
|
-
*/
|
|
517
|
-
async getNamespaceIdentity(namespaceId) {
|
|
518
|
-
const node = await this.getNodeIdentity();
|
|
519
|
-
return { namespaceId, publicKey: node.publicKey, account: node.accountId };
|
|
520
|
-
}
|
|
521
410
|
/**
|
|
522
411
|
* Ask this node to run one method on a member's behalf, under a warrant that
|
|
523
412
|
* member signed.
|
|
@@ -924,113 +813,6 @@ export class AdminApiClient {
|
|
|
924
813
|
const response = await this.httpClient.get(query ? `${base}?${query}` : base);
|
|
925
814
|
return response.members;
|
|
926
815
|
}
|
|
927
|
-
/**
|
|
928
|
-
* Where `authorDeviceKey` stands in its warrant-nonce sequence in `contextId`.
|
|
929
|
-
*
|
|
930
|
-
* # What this is for
|
|
931
|
-
*
|
|
932
|
-
* The nonce is the one input to a warrant that is stateful on the *client*.
|
|
933
|
-
* Everything else an author needs it either holds (its keys, the method, the
|
|
934
|
-
* args) or can read from the relay ({@link RelayClient.describe} gives the
|
|
935
|
-
* executor account and the grant). So an author that has lost its counter —
|
|
936
|
-
* a browser keyholder in partitioned or periodically cleared storage, which
|
|
937
|
-
* is its normal lifecycle rather than a fault — otherwise has exactly one
|
|
938
|
-
* strategy: guess upward and burn a refused round trip per wrong guess, with
|
|
939
|
-
* each refusal indistinguishable at the API from a genuinely spent nonce.
|
|
940
|
-
*
|
|
941
|
-
* Read {@link WarrantNonceOpen.nextNonce} and mint there. If you still hold
|
|
942
|
-
* your own counter, mint at `max(ownNext, nextNonce)`: nonce state folds per
|
|
943
|
-
* peer, so a node can only be BEHIND a client whose warrants it has not
|
|
944
|
-
* applied — never ahead of the truth. An author spread across several relays
|
|
945
|
-
* asks each and takes the highest answer. {@link createRecoveringNonceSource}
|
|
946
|
-
* does all of that.
|
|
947
|
-
*
|
|
948
|
-
* Purely a read. It spends nothing and advances nothing, so it may be called
|
|
949
|
-
* as often as you like.
|
|
950
|
-
*
|
|
951
|
-
* # Forward compatibility — this route is NEW
|
|
952
|
-
*
|
|
953
|
-
* It is not on core `master` yet, so a current node answers **404**, which
|
|
954
|
-
* this method surfaces as {@link WarrantNonceRouteUnavailableError} rather
|
|
955
|
-
* than as an opaque {@link HTTPError}. That distinction matters because the
|
|
956
|
-
* handler itself never 404s: a device with no row is a `200` with
|
|
957
|
-
* `seen: false`, and a malformed id is a `400`. So a 404 here means the node
|
|
958
|
-
* does not serve the route at all — an older build, or a deployment that has
|
|
959
|
-
* not re-mounted it — and the answer is to fall back to a local counter, not
|
|
960
|
-
* to retry.
|
|
961
|
-
*
|
|
962
|
-
* # Authentication
|
|
963
|
-
*
|
|
964
|
-
* On the PROTECTED admin router, unlike the two delegated-execution routes it
|
|
965
|
-
* sits beside: it takes another principal's device key as input and reports
|
|
966
|
-
* that principal's activity. A `--public-intents` deployment does not serve
|
|
967
|
-
* this anonymously, so an admin credential is required.
|
|
968
|
-
*
|
|
969
|
-
* @param authorDeviceKey the device's signing key, base58 (43-44 chars) or
|
|
970
|
-
* hex (exactly 64). The node accepts either; the two never collide.
|
|
971
|
-
*/
|
|
972
|
-
async getWarrantNonce(contextId, authorDeviceKey) {
|
|
973
|
-
const path = `/admin-api/contexts/${pathSegment(contextId)}/warrant-nonce/${pathSegment(authorDeviceKey)}`;
|
|
974
|
-
// Read as TEXT, not JSON. `JSON.parse` would turn the `u64` nonces into
|
|
975
|
-
// doubles before this code ever sees them, rounding anything past 2^53 into
|
|
976
|
-
// a value that looks ordinary and is refused forever. The digits have to be
|
|
977
|
-
// taken from the bytes.
|
|
978
|
-
let raw;
|
|
979
|
-
try {
|
|
980
|
-
raw = await this.httpClient.get(path, { parse: 'text' });
|
|
981
|
-
}
|
|
982
|
-
catch (error) {
|
|
983
|
-
if (error instanceof HTTPError && error.status === 404) {
|
|
984
|
-
throw new WarrantNonceRouteUnavailableError(path, error);
|
|
985
|
-
}
|
|
986
|
-
throw error;
|
|
987
|
-
}
|
|
988
|
-
return parseWarrantNonce(raw);
|
|
989
|
-
}
|
|
990
|
-
/**
|
|
991
|
-
* The same answer, asked for the way a delegated client has to ask.
|
|
992
|
-
*
|
|
993
|
-
* {@link getWarrantNonce} is on the protected admin router, so a keyholder
|
|
994
|
-
* writing through a relay — the client this whole facility exists for — is
|
|
995
|
-
* refused it: measured `403` for a session carrying exactly
|
|
996
|
-
* `context:intent`, `context:query`, `context:subscribe`. That client has no
|
|
997
|
-
* admin credential and cannot be given one without defeating the point of the
|
|
998
|
-
* delegated surface.
|
|
999
|
-
*
|
|
1000
|
-
* What it does have is the credential proving the device is its own, which it
|
|
1001
|
-
* already sends on every write. Presenting it here makes the request
|
|
1002
|
-
* self-scoped: you learn a device's position only by proving, at call time,
|
|
1003
|
-
* that the device is yours. So there is no oracle to protect against, and no
|
|
1004
|
-
* reason for an admin gate.
|
|
1005
|
-
*
|
|
1006
|
-
* The device key is NOT a parameter. It is read from the verified
|
|
1007
|
-
* certificate server-side, which is the only reading that cannot disagree
|
|
1008
|
-
* with the signature — a key supplied alongside would be a second spelling of
|
|
1009
|
-
* the same fact, and two spellings can differ.
|
|
1010
|
-
*
|
|
1011
|
-
* Returns the identical shape {@link getWarrantNonce} returns, so a caller can
|
|
1012
|
-
* swap one for the other without touching what it does with the answer.
|
|
1013
|
-
*
|
|
1014
|
-
* @param authorProof hex-encoded borsh `AccountProof<DeviceCert>` — the same
|
|
1015
|
-
* value passed as `authorProof` to the relay client.
|
|
1016
|
-
*/
|
|
1017
|
-
async getWarrantNonceAsAuthor(contextId, authorProof) {
|
|
1018
|
-
const path = `/admin-api/contexts/${pathSegment(contextId)}/warrant-nonce`;
|
|
1019
|
-
// Text, not JSON, for the reason given in `getWarrantNonce`: `JSON.parse`
|
|
1020
|
-
// rounds a u64 past 2^53 into a nonce that looks ordinary and is refused
|
|
1021
|
-
// for ever.
|
|
1022
|
-
let raw;
|
|
1023
|
-
try {
|
|
1024
|
-
raw = await this.httpClient.post(path, { authorProof }, { parse: 'text' });
|
|
1025
|
-
}
|
|
1026
|
-
catch (error) {
|
|
1027
|
-
if (error instanceof HTTPError && error.status === 404) {
|
|
1028
|
-
throw new WarrantNonceRouteUnavailableError(path, error);
|
|
1029
|
-
}
|
|
1030
|
-
throw error;
|
|
1031
|
-
}
|
|
1032
|
-
return parseWarrantNonce(raw);
|
|
1033
|
-
}
|
|
1034
816
|
/**
|
|
1035
817
|
* Member accounts in the group and the devices bound to each. An admin node
|
|
1036
818
|
* sees every account; a plain member only its own. `403` if this node is not a
|
|
@@ -1326,13 +1108,6 @@ export class AdminApiClient {
|
|
|
1326
1108
|
async teeAttest(request) {
|
|
1327
1109
|
return unwrap(await this.httpClient.post('/admin-api/tee/attest', request));
|
|
1328
1110
|
}
|
|
1329
|
-
/**
|
|
1330
|
-
* @deprecated Nodes no longer serve `/admin-api/tee/verify-quote` (removed in
|
|
1331
|
-
* core#3262), so this fails. Verify quotes with `createQuoteVerifier`.
|
|
1332
|
-
*/
|
|
1333
|
-
async teeVerifyQuote(request) {
|
|
1334
|
-
return unwrap(await this.httpClient.post('/admin-api/tee/verify-quote', request));
|
|
1335
|
-
}
|
|
1336
1111
|
// ---- Network ----
|
|
1337
1112
|
async getPeersCount() {
|
|
1338
1113
|
return this.httpClient.get('/admin-api/peers');
|