@calimero-network/mero-js 18.1.1 → 18.3.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 +47 -0
- package/dist/admin-api/admin-client.d.ts +22 -1
- package/dist/admin-api/admin-client.js +27 -1
- package/dist/admin-api/admin-types.d.ts +42 -2
- package/dist/capabilities.d.ts +14 -2
- package/dist/capabilities.js +15 -3
- package/dist/cloud/cloud-client.d.ts +264 -0
- package/dist/cloud/cloud-client.js +319 -1
- package/dist/cloud/connect.d.ts +94 -0
- package/dist/cloud/connect.js +150 -0
- package/dist/cloud/index.d.ts +3 -1
- package/dist/cloud/index.js +1 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +631 -29
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/index.mjs +631 -29
- package/dist/index.mjs.map +3 -3
- package/dist/mero-js.d.ts +28 -0
- package/dist/mero-js.js +17 -0
- package/dist/relay/index.d.ts +4 -0
- package/dist/relay/index.js +2 -0
- package/dist/relay/nonce-source.d.ts +60 -0
- package/dist/relay/nonce-source.js +95 -0
- package/dist/relay/relay-client.d.ts +141 -0
- package/dist/relay/relay-client.js +157 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -89,6 +89,53 @@ Every failure surfaces as a typed error:
|
|
|
89
89
|
you only see a `401` if the refresh itself fails. Full details in the
|
|
90
90
|
[error model reference](https://calimero-network.github.io/mero-js/reference/error-model/).
|
|
91
91
|
|
|
92
|
+
## Writing without a node
|
|
93
|
+
|
|
94
|
+
The quickstart above assumes you have a node and a credential on it. A browser
|
|
95
|
+
tab, a phone, or an agent has neither — the runtime cannot compile the app's WASM
|
|
96
|
+
against materialized state, and never received the scope key that seals the
|
|
97
|
+
deltas. So there is a second connection shape: sign in to **Calimero Cloud**, and
|
|
98
|
+
write through an attested TEE **relay** using
|
|
99
|
+
[delegated execution](https://calimero-network.github.io/core/protocol/delegated-authorship/).
|
|
100
|
+
|
|
101
|
+
You sign a *warrant* locally — your consent for one specific intent, once — the
|
|
102
|
+
relay runs the method as **your** principal, and the resulting change is
|
|
103
|
+
attributed to your account and device. Your signing key never leaves the
|
|
104
|
+
process; only signatures do.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { connectCloud } from '@calimero-network/mero-js';
|
|
108
|
+
|
|
109
|
+
const connection = await connectCloud({
|
|
110
|
+
googleIdToken, // or a stored sessionToken
|
|
111
|
+
authorAccount, // your account, hex
|
|
112
|
+
authorProof, // your AccountProof<DeviceCert>, hex borsh
|
|
113
|
+
deviceSecret, // your device's ed25519 seed, hex — never transmitted
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const { rootHash, returns } = await connection.execute(contextId, 'set', {
|
|
117
|
+
key: 'greeting',
|
|
118
|
+
value: 'hello',
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`connectCloud` replaces the **node-URL prompt**, not the authentication: it asks
|
|
123
|
+
the cloud which namespaces the account owns and which relay serves them, then
|
|
124
|
+
builds the relay client. `sdk.cloud` exposes the same cloud API from an existing
|
|
125
|
+
`MeroJs` instance for apps that have both a node and a cloud account, and
|
|
126
|
+
`RelayClient` is available directly for a relay you were told about out of band.
|
|
127
|
+
|
|
128
|
+
One precondition is not yours to satisfy: an admin of the namespace must grant
|
|
129
|
+
the relay `CAN_AUTHOR_ON_BEHALF` (core implies it from nothing — not from
|
|
130
|
+
membership, not from admin). Until then `connectCloud` throws naming the account
|
|
131
|
+
that needs the grant, and `relay.describe(contextId)` reports
|
|
132
|
+
`canAuthorOnBehalf: false` so a UI can say so rather than failing a write.
|
|
133
|
+
|
|
134
|
+
See the [cloud client](https://calimero-network.github.io/mero-js/reference/cloud/),
|
|
135
|
+
[relay client](https://calimero-network.github.io/mero-js/reference/relay/) and
|
|
136
|
+
[connectCloud](https://calimero-network.github.io/mero-js/reference/connect-cloud/)
|
|
137
|
+
references — including why a warrant nonce must never restart.
|
|
138
|
+
|
|
92
139
|
## Lower-level HTTP client
|
|
93
140
|
|
|
94
141
|
If you need raw HTTP against a node (without the `MeroJs` facade), the SDK
|
|
@@ -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, CreateContextAliasRequest, CreateApplicationAliasRequest, 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, 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, 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, CreateContextAliasRequest, CreateApplicationAliasRequest, 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, 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
|
|
@@ -62,6 +62,7 @@ export declare class AdminApiClient {
|
|
|
62
62
|
joinContext(contextId: string): Promise<JoinContextResponseData>;
|
|
63
63
|
getContextGroup(contextId: string): Promise<ContextGroupResponseData>;
|
|
64
64
|
getContextStorage(contextId: string): Promise<ContextStorageResponseData>;
|
|
65
|
+
/** Sync one context, or every context when `contextId` is omitted. */
|
|
65
66
|
syncContext(contextId?: string): Promise<void>;
|
|
66
67
|
/**
|
|
67
68
|
* Kick off a full state re-pull for a context (operator recovery for a
|
|
@@ -153,6 +154,26 @@ export declare class AdminApiClient {
|
|
|
153
154
|
* {@link PerformIntentRequest.warrant}.
|
|
154
155
|
*/
|
|
155
156
|
performIntent(contextId: string, request: PerformIntentRequest): Promise<PerformIntentResponseData>;
|
|
157
|
+
/**
|
|
158
|
+
* Ask what this node can do for a member in `contextId`, before they sign
|
|
159
|
+
* anything.
|
|
160
|
+
*
|
|
161
|
+
* The two facts {@link performIntent} needs and a client cannot derive: whose
|
|
162
|
+
* account goes in the warrant's `executor`, and whether this node holds
|
|
163
|
+
* `CAN_AUTHOR_ON_BEHALF` on the owning group. `canAuthorOnBehalf: false` comes
|
|
164
|
+
* back as an answer rather than an error — it is the default state of every
|
|
165
|
+
* context — so a caller can say "an admin of `groupId` must grant it to
|
|
166
|
+
* `executorAccount`" instead of presenting a warrant that will be refused
|
|
167
|
+
* after the author has spent a nonce on it.
|
|
168
|
+
*
|
|
169
|
+
* `404` means the context belongs to no group, or this node holds no account
|
|
170
|
+
* yet; either way it can be named as no warrant's executor.
|
|
171
|
+
*
|
|
172
|
+
* For a caller with no credential on the node at all — a browser tab holding
|
|
173
|
+
* one signing key — use `RelayClient`, which reads this same route on a node
|
|
174
|
+
* run as a relay.
|
|
175
|
+
*/
|
|
176
|
+
getIntentRelay(contextId: string): Promise<IntentRelayInfo>;
|
|
156
177
|
listNamespacesForApplication(applicationId: string): Promise<ListNamespacesResponseData>;
|
|
157
178
|
createNamespace(request: CreateNamespaceRequest): Promise<CreateNamespaceResponseData>;
|
|
158
179
|
deleteNamespace(namespaceId: string): Promise<DeleteNamespaceResponseData>;
|
|
@@ -217,8 +217,12 @@ export class AdminApiClient {
|
|
|
217
217
|
async getContextStorage(contextId) {
|
|
218
218
|
return unwrap(await this.httpClient.get(`/admin-api/contexts/${contextId}/storage`));
|
|
219
219
|
}
|
|
220
|
+
/** Sync one context, or every context when `contextId` is omitted. */
|
|
220
221
|
async syncContext(contextId) {
|
|
221
|
-
|
|
222
|
+
// Sync-all is `/contexts/sync` with no trailing slash - core routes the two
|
|
223
|
+
// spellings separately and 404s the trailing one.
|
|
224
|
+
const path = contextId ? `/admin-api/contexts/sync/${contextId}` : '/admin-api/contexts/sync';
|
|
225
|
+
await this.httpClient.post(path, {});
|
|
222
226
|
}
|
|
223
227
|
/**
|
|
224
228
|
* Kick off a full state re-pull for a context (operator recovery for a
|
|
@@ -406,6 +410,28 @@ export class AdminApiClient {
|
|
|
406
410
|
async performIntent(contextId, request) {
|
|
407
411
|
return unwrap(await this.httpClient.post(`/admin-api/contexts/${contextId}/intents`, request));
|
|
408
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Ask what this node can do for a member in `contextId`, before they sign
|
|
415
|
+
* anything.
|
|
416
|
+
*
|
|
417
|
+
* The two facts {@link performIntent} needs and a client cannot derive: whose
|
|
418
|
+
* account goes in the warrant's `executor`, and whether this node holds
|
|
419
|
+
* `CAN_AUTHOR_ON_BEHALF` on the owning group. `canAuthorOnBehalf: false` comes
|
|
420
|
+
* back as an answer rather than an error — it is the default state of every
|
|
421
|
+
* context — so a caller can say "an admin of `groupId` must grant it to
|
|
422
|
+
* `executorAccount`" instead of presenting a warrant that will be refused
|
|
423
|
+
* after the author has spent a nonce on it.
|
|
424
|
+
*
|
|
425
|
+
* `404` means the context belongs to no group, or this node holds no account
|
|
426
|
+
* yet; either way it can be named as no warrant's executor.
|
|
427
|
+
*
|
|
428
|
+
* For a caller with no credential on the node at all — a browser tab holding
|
|
429
|
+
* one signing key — use `RelayClient`, which reads this same route on a node
|
|
430
|
+
* run as a relay.
|
|
431
|
+
*/
|
|
432
|
+
async getIntentRelay(contextId) {
|
|
433
|
+
return unwrap(await this.httpClient.get(`/admin-api/contexts/${contextId}/intents`));
|
|
434
|
+
}
|
|
409
435
|
async listNamespacesForApplication(applicationId) {
|
|
410
436
|
return unwrap(await this.httpClient.get(`/admin-api/namespaces/for-application/${applicationId}`));
|
|
411
437
|
}
|
|
@@ -401,6 +401,40 @@ export interface PerformIntentResponseData {
|
|
|
401
401
|
/** The method's own return value, if it had one. */
|
|
402
402
|
returns: unknown | null;
|
|
403
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* What a node says about running intents in one context — read before minting a
|
|
406
|
+
* warrant, not after being refused one.
|
|
407
|
+
*
|
|
408
|
+
* Every input to a warrant is something the author already holds except two:
|
|
409
|
+
* whose name goes in `executor`, and whether that node may act here at all. Both
|
|
410
|
+
* belong to the node, so both are answered here, on the path the intent will be
|
|
411
|
+
* presented to.
|
|
412
|
+
*
|
|
413
|
+
* Reading it first is what lets a client fail before signing. A warrant spends a
|
|
414
|
+
* nonce from a monotonic per-device sequence, and one naming the wrong executor
|
|
415
|
+
* is unspendable — the number is gone and the write never happened.
|
|
416
|
+
*/
|
|
417
|
+
export interface IntentRelayInfo {
|
|
418
|
+
/**
|
|
419
|
+
* The account a warrant for this node must name as its `executor`, 64 hex.
|
|
420
|
+
*
|
|
421
|
+
* An account, not the node's signing key: one of its processes re-keying must
|
|
422
|
+
* not void warrants already issued to it.
|
|
423
|
+
*/
|
|
424
|
+
executorAccount: string;
|
|
425
|
+
/**
|
|
426
|
+
* Whether this node holds `CAN_AUTHOR_ON_BEHALF` on the group owning the
|
|
427
|
+
* context.
|
|
428
|
+
*
|
|
429
|
+
* `false` is an answer, not an error, and it is the *default* state of every
|
|
430
|
+
* context — the capability is implied by neither membership nor admin, and is
|
|
431
|
+
* not propagated by the subgroup cascade. A client reads it to tell "ask an
|
|
432
|
+
* admin of `groupId`" from "retry later".
|
|
433
|
+
*/
|
|
434
|
+
canAuthorOnBehalf: boolean;
|
|
435
|
+
/** The group whose admin must grant that capability, 64 hex. */
|
|
436
|
+
groupId: string;
|
|
437
|
+
}
|
|
404
438
|
export interface NodeIdentity {
|
|
405
439
|
/**
|
|
406
440
|
* The account this node writes as, 64 hex characters. This — not
|
|
@@ -908,9 +942,15 @@ export interface MemberMigrationReport {
|
|
|
908
942
|
migrationFailed?: MigrationFailureReason;
|
|
909
943
|
}
|
|
910
944
|
export interface MemberMigrationStatusEntry {
|
|
945
|
+
/** The reporting device's signing KEY, bs58. One account with two devices
|
|
946
|
+
* appears as two rows: the cohort is replicas, not people. */
|
|
911
947
|
peer: string;
|
|
912
|
-
/**
|
|
913
|
-
|
|
948
|
+
/** The ACCOUNT `peer` speaks for, 64 hex - join it to `listGroupMembers` to
|
|
949
|
+
* name a person. Absent from nodes predating the field. */
|
|
950
|
+
account?: string;
|
|
951
|
+
/** Freshest reported facts. Omitted (not `null`) when the member's state is
|
|
952
|
+
* `unknown`, so test falsiness rather than `=== null`. */
|
|
953
|
+
report?: MemberMigrationReport | null;
|
|
914
954
|
state: MemberMigrationState;
|
|
915
955
|
}
|
|
916
956
|
export interface MigrationStatusRollup {
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Capability bits as defined by core's `MemberCapabilities`.
|
|
3
3
|
*
|
|
4
|
-
* The per-member value is a u32 bitmask. Core currently assigns bits 0..=
|
|
5
|
-
* (the entries below); bits
|
|
4
|
+
* The per-member value is a u32 bitmask. Core currently assigns bits 0..=9
|
|
5
|
+
* (the entries below); bits 10 and above are unassigned — do not repurpose
|
|
6
6
|
* them for application data, as a future core release may claim them.
|
|
7
7
|
*/
|
|
8
8
|
export declare const CAPABILITIES: {
|
|
@@ -15,6 +15,18 @@ export declare const CAPABILITIES: {
|
|
|
15
15
|
readonly CAN_DELETE_SUBGROUP: number;
|
|
16
16
|
readonly CAN_MANAGE_VISIBILITY: number;
|
|
17
17
|
readonly CAN_MANAGE_METADATA: number;
|
|
18
|
+
/**
|
|
19
|
+
* Publish writes attributed to ANOTHER member, under a warrant that member
|
|
20
|
+
* signed — the grant delegated execution runs on.
|
|
21
|
+
*
|
|
22
|
+
* Implied by nothing: not by membership, not by admin, and not propagated by
|
|
23
|
+
* the subgroup-admit cascade. So every group is authorship-closed until an
|
|
24
|
+
* admin sets this bit, and a relay without it is refused at
|
|
25
|
+
* `performIntent` before anything executes. That is why it is worth naming
|
|
26
|
+
* here rather than leaving callers to write `1 << 9`: a client granting it
|
|
27
|
+
* has to be able to say what it is granting.
|
|
28
|
+
*/
|
|
29
|
+
readonly CAN_AUTHOR_ON_BEHALF: number;
|
|
18
30
|
};
|
|
19
31
|
export type CapabilityName = keyof typeof CAPABILITIES;
|
|
20
32
|
export type CapabilityBit = (typeof CAPABILITIES)[CapabilityName];
|
package/dist/capabilities.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Member capability bitmask constants — mirrors core's `MemberCapabilities`
|
|
2
2
|
// (crates/context/config/src/lib.rs). The value stored per-member is a u32
|
|
3
|
-
// bitmask. Core currently assigns bits 0..=
|
|
3
|
+
// bitmask. Core currently assigns bits 0..=9; bits 10 and above are unassigned
|
|
4
4
|
// and may be claimed by future core versions, so an application MUST NOT
|
|
5
5
|
// assume any particular bit is safe for its own use unless core documents it
|
|
6
6
|
// as reserved for applications.
|
|
7
7
|
/**
|
|
8
8
|
* Capability bits as defined by core's `MemberCapabilities`.
|
|
9
9
|
*
|
|
10
|
-
* The per-member value is a u32 bitmask. Core currently assigns bits 0..=
|
|
11
|
-
* (the entries below); bits
|
|
10
|
+
* The per-member value is a u32 bitmask. Core currently assigns bits 0..=9
|
|
11
|
+
* (the entries below); bits 10 and above are unassigned — do not repurpose
|
|
12
12
|
* them for application data, as a future core release may claim them.
|
|
13
13
|
*/
|
|
14
14
|
export const CAPABILITIES = {
|
|
@@ -21,6 +21,18 @@ export const CAPABILITIES = {
|
|
|
21
21
|
CAN_DELETE_SUBGROUP: 1 << 6,
|
|
22
22
|
CAN_MANAGE_VISIBILITY: 1 << 7,
|
|
23
23
|
CAN_MANAGE_METADATA: 1 << 8,
|
|
24
|
+
/**
|
|
25
|
+
* Publish writes attributed to ANOTHER member, under a warrant that member
|
|
26
|
+
* signed — the grant delegated execution runs on.
|
|
27
|
+
*
|
|
28
|
+
* Implied by nothing: not by membership, not by admin, and not propagated by
|
|
29
|
+
* the subgroup-admit cascade. So every group is authorship-closed until an
|
|
30
|
+
* admin sets this bit, and a relay without it is refused at
|
|
31
|
+
* `performIntent` before anything executes. That is why it is worth naming
|
|
32
|
+
* here rather than leaving callers to write `1 << 9`: a client granting it
|
|
33
|
+
* has to be able to say what it is granting.
|
|
34
|
+
*/
|
|
35
|
+
CAN_AUTHOR_ON_BEHALF: 1 << 9,
|
|
24
36
|
};
|
|
25
37
|
/**
|
|
26
38
|
* Returns true if `mask` has every bit of `cap` set. Both operands are
|
|
@@ -1,5 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calimero Cloud (MDMA) client.
|
|
3
|
+
*
|
|
4
|
+
* # Why the SDK knows about the cloud at all
|
|
5
|
+
*
|
|
6
|
+
* Delegated execution lets an account that holds one signing key write into a
|
|
7
|
+
* context without running a node — but only through a *relay*, and a relay is
|
|
8
|
+
* something the client has to be told about. A warrant names an executor
|
|
9
|
+
* account, so a client that cannot discover which relay serves its namespace,
|
|
10
|
+
* and which account that relay writes as, cannot mint a usable warrant at all.
|
|
11
|
+
*
|
|
12
|
+
* For a self-hosted namespace the operator hands those two facts over out of
|
|
13
|
+
* band. For the hosted tier the cloud is the directory: the user signs in, the
|
|
14
|
+
* cloud knows which namespaces they own and which attested TEE nodes are
|
|
15
|
+
* assigned to each, and it answers with the relay's URL and executor account.
|
|
16
|
+
* That is what this client is for — it is the discovery half of
|
|
17
|
+
* {@link RelayClient}, not a second way to talk to a node.
|
|
18
|
+
*
|
|
19
|
+
* # What it deliberately is not
|
|
20
|
+
*
|
|
21
|
+
* It never sees a warrant, a device secret, or an intent. Discovery goes
|
|
22
|
+
* through the cloud; the write goes straight from the client to the relay. A
|
|
23
|
+
* cloud that proxied intents would read every method and argument its users
|
|
24
|
+
* ever sent, and would be a throughput bottleneck on a path that has no reason
|
|
25
|
+
* to involve it.
|
|
26
|
+
*/
|
|
1
27
|
export interface CloudClientConfig {
|
|
2
28
|
cloudBaseUrl?: string;
|
|
29
|
+
/**
|
|
30
|
+
* An MDMA session token to start out authenticated with — from a previous
|
|
31
|
+
* {@link CloudClient.signInWithGoogle}, persisted by the app.
|
|
32
|
+
*
|
|
33
|
+
* Sessions are 7-day JWTs with a rolling refresh, so a stored one is usually
|
|
34
|
+
* still live and re-running the Google flow on every launch is avoidable.
|
|
35
|
+
*/
|
|
36
|
+
sessionToken?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Called whenever the session token changes — a sign-in, an explicit
|
|
39
|
+
* refresh, or a rolling refresh the server performed mid-request. Persist it
|
|
40
|
+
* here; errors thrown are swallowed so a storage failure cannot mask the
|
|
41
|
+
* response the caller is waiting for.
|
|
42
|
+
*/
|
|
43
|
+
onSession?: (session: CloudSession | null) => void | Promise<void>;
|
|
44
|
+
/** Injected for tests and non-browser runtimes. Defaults to global `fetch`. */
|
|
45
|
+
fetch?: typeof fetch;
|
|
46
|
+
/** Per-request timeout. Defaults to 10s, as the node clients do. */
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
/** Who is signed in, and until when. */
|
|
50
|
+
export interface CloudSession {
|
|
51
|
+
sessionToken: string;
|
|
52
|
+
/** Epoch **seconds**, as MDMA reports it — not milliseconds. */
|
|
53
|
+
expiresAt: number;
|
|
54
|
+
user: {
|
|
55
|
+
email: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
picture?: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** One namespace the signed-in user owns. */
|
|
61
|
+
export interface CloudNamespace {
|
|
62
|
+
namespaceId: string;
|
|
63
|
+
/** Always equal to `namespaceId`; MDMA keeps it for wire compatibility. */
|
|
64
|
+
groupId: string;
|
|
65
|
+
/** `'enabled' | 'disabled' | 'none'` — `'none'` means HA was never enabled. */
|
|
66
|
+
haStatus: string;
|
|
67
|
+
haEnabledAt?: string | null;
|
|
68
|
+
contexts: string[];
|
|
69
|
+
fleetReplicas?: {
|
|
70
|
+
current?: number;
|
|
71
|
+
limit?: number;
|
|
72
|
+
} | unknown;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* One attested node the cloud has assigned to a namespace, described as a
|
|
76
|
+
* relay.
|
|
77
|
+
*
|
|
78
|
+
* `authorshipReady` is the field that decides whether a write can happen, and
|
|
79
|
+
* it is reported rather than assumed because it is not the cloud's to grant:
|
|
80
|
+
* `CAN_AUTHOR_ON_BEHALF` is a governance capability, set by an admin of the
|
|
81
|
+
* namespace, and the cloud only relays what the node observed. A relay that is
|
|
82
|
+
* `active` with `authorshipReady: false` is a healthy node waiting for a grant.
|
|
83
|
+
*/
|
|
84
|
+
export interface CloudRelay {
|
|
85
|
+
peerId: string;
|
|
86
|
+
/** Base URL to present intents to, or `null` when the cloud knows none yet. */
|
|
87
|
+
relayUrl: string | null;
|
|
88
|
+
/** The account a warrant for this relay must name as its `executor`, hex. */
|
|
89
|
+
executorAccount: string | null;
|
|
90
|
+
/** `'assigned'` (admitted pending confirm) or `'active'`. */
|
|
91
|
+
status: string;
|
|
92
|
+
/** Whether this relay holds `CAN_AUTHOR_ON_BEHALF` on the namespace. */
|
|
93
|
+
authorshipReady: boolean;
|
|
94
|
+
lastSeenAt?: string | null;
|
|
95
|
+
confirmedAt?: string | null;
|
|
96
|
+
}
|
|
97
|
+
/** One namespace of the caller's that a machine is serving. */
|
|
98
|
+
export interface CloudMachineNamespace {
|
|
99
|
+
namespaceId: string;
|
|
100
|
+
/** `'assigned'` (admitted pending confirm) or `'active'`. */
|
|
101
|
+
status: string;
|
|
102
|
+
/** Whether the machine holds `CAN_AUTHOR_ON_BEHALF` on this namespace. */
|
|
103
|
+
authorshipReady: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Whether the machine's heartbeat for this namespace is recent.
|
|
106
|
+
*
|
|
107
|
+
* The same freshness rule the per-plan fleet cap uses, so a replica that
|
|
108
|
+
* stopped polling reads as stale here rather than continuing to look
|
|
109
|
+
* healthy — otherwise "my writes stopped working" has no visible cause.
|
|
110
|
+
*/
|
|
111
|
+
fresh: boolean;
|
|
112
|
+
confirmedAt?: string | null;
|
|
113
|
+
lastSeenAt?: string | null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* One attested machine serving this account.
|
|
117
|
+
*
|
|
118
|
+
* A narrow view on purpose: the zone, machine type, instance name, public IP
|
|
119
|
+
* and KMS ids are operator data with no client use, and a cloud user is not an
|
|
120
|
+
* operator.
|
|
121
|
+
*/
|
|
122
|
+
export interface CloudMachine {
|
|
123
|
+
peerId: string;
|
|
124
|
+
relayUrl: string | null;
|
|
125
|
+
executorAccount: string | null;
|
|
126
|
+
/**
|
|
127
|
+
* Whether this machine can write for the caller *somewhere* — a URL, an
|
|
128
|
+
* executor account, and the grant on at least one namespace.
|
|
129
|
+
*
|
|
130
|
+
* Folded here so a client does not re-derive the three-way precondition and
|
|
131
|
+
* get a refusal it cannot explain.
|
|
132
|
+
*/
|
|
133
|
+
canExecute: boolean;
|
|
134
|
+
/** Only the caller's own namespaces, even on a machine shared with others. */
|
|
135
|
+
namespaces: CloudMachineNamespace[];
|
|
3
136
|
}
|
|
4
137
|
export interface EnableHAOptions {
|
|
5
138
|
groupId: string;
|
|
@@ -13,7 +146,138 @@ export interface DisableHAOptions {
|
|
|
13
146
|
}
|
|
14
147
|
export declare class CloudClient {
|
|
15
148
|
private baseUrl;
|
|
149
|
+
private session;
|
|
150
|
+
private readonly onSession?;
|
|
151
|
+
private readonly fetchImpl;
|
|
152
|
+
private readonly timeoutMs;
|
|
16
153
|
constructor(config?: CloudClientConfig);
|
|
154
|
+
/** The current session, or `null` when nobody is signed in. */
|
|
155
|
+
getSession(): CloudSession | null;
|
|
156
|
+
/** Whether a session token is held. Says nothing about the server agreeing. */
|
|
157
|
+
isSignedIn(): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Exchange a Google OAuth2 ID token for an MDMA session.
|
|
160
|
+
*
|
|
161
|
+
* The app owns the Google flow (One Tap, an auth-code popup, a native
|
|
162
|
+
* browser) and hands the resulting ID token here once. Everything after this
|
|
163
|
+
* uses the returned session token, so the Google credential does not have to
|
|
164
|
+
* be kept or re-minted per call.
|
|
165
|
+
*/
|
|
166
|
+
signInWithGoogle(idToken: string): Promise<CloudSession>;
|
|
167
|
+
/**
|
|
168
|
+
* Refresh the session token explicitly.
|
|
169
|
+
*
|
|
170
|
+
* Rarely needed: MDMA rolling-refreshes on ordinary authenticated calls and
|
|
171
|
+
* this client adopts the replacement automatically. Use it when an app wants
|
|
172
|
+
* a fresh token before going idle rather than on the next request.
|
|
173
|
+
*/
|
|
174
|
+
refreshSession(): Promise<CloudSession>;
|
|
175
|
+
/**
|
|
176
|
+
* Revoke this session server-side and forget it locally.
|
|
177
|
+
*
|
|
178
|
+
* Local state is cleared even if the server call fails: the caller asked to
|
|
179
|
+
* be signed out, and keeping a token they believe is gone is the worse of the
|
|
180
|
+
* two failures.
|
|
181
|
+
*/
|
|
182
|
+
signOut(): Promise<void>;
|
|
183
|
+
/** Namespaces the signed-in user owns. */
|
|
184
|
+
getMyNamespaces(): Promise<CloudNamespace[]>;
|
|
185
|
+
/**
|
|
186
|
+
* The relays serving one namespace: where to present an intent, and as whom.
|
|
187
|
+
*
|
|
188
|
+
* Everything a client needs in order to mint a warrant it can actually
|
|
189
|
+
* spend. Filter on `authorshipReady` before signing — a warrant presented to
|
|
190
|
+
* a relay with no grant is refused *after* the author has burned a nonce from
|
|
191
|
+
* its monotonic sequence on it.
|
|
192
|
+
*/
|
|
193
|
+
getNamespaceRelays(namespaceId: string): Promise<CloudRelay[]>;
|
|
194
|
+
/**
|
|
195
|
+
* The first relay for `namespaceId` that can actually run an intent, or
|
|
196
|
+
* `null`.
|
|
197
|
+
*
|
|
198
|
+
* "Can actually run one" is three conditions, and a client that checks fewer
|
|
199
|
+
* gets a refusal it cannot explain: the cloud must know a URL, the relay must
|
|
200
|
+
* have reported the executor account a warrant has to name, and the grant
|
|
201
|
+
* must be in place.
|
|
202
|
+
*/
|
|
203
|
+
findExecutingRelay(namespaceId: string): Promise<CloudRelay | null>;
|
|
204
|
+
/**
|
|
205
|
+
* Every attested machine currently serving something this account owns.
|
|
206
|
+
*
|
|
207
|
+
* The account-wide view of the same rows {@link getNamespaceRelays} returns
|
|
208
|
+
* per namespace, keyed by machine — so a UI can answer "what is running for
|
|
209
|
+
* me?" in one call. `canExecute` folds the three-way precondition, so a
|
|
210
|
+
* caller does not re-derive it.
|
|
211
|
+
*
|
|
212
|
+
* Only the caller's own namespaces appear on each machine. A plan's
|
|
213
|
+
* `usersPerMachine` is greater than one, so a fleet node routinely serves
|
|
214
|
+
* several accounts at once.
|
|
215
|
+
*/
|
|
216
|
+
getMyMachines(): Promise<CloudMachine[]>;
|
|
217
|
+
/**
|
|
218
|
+
* Claim a namespace under this account, proving ownership.
|
|
219
|
+
*
|
|
220
|
+
* The cloud has no other way to learn a namespace exists — namespaces are
|
|
221
|
+
* created on a node, and there is deliberately no "create namespace" in the
|
|
222
|
+
* cloud. `ownershipProof` comes from
|
|
223
|
+
* `admin.issueNamespaceOwnershipProof(namespaceId, …)` on a node that is a
|
|
224
|
+
* **direct admin** of that namespace; merod refuses to issue one otherwise.
|
|
225
|
+
*
|
|
226
|
+
* Pass merod's response object straight through. The cloud accepts core's
|
|
227
|
+
* `signerPublicKey`/`signedPayload` casing as well as snake_case, so there is
|
|
228
|
+
* no re-keying step — and getting that wrong used to surface as a `422`
|
|
229
|
+
* naming three missing fields.
|
|
230
|
+
*
|
|
231
|
+
* Idempotent for the same account. A namespace already claimed by someone
|
|
232
|
+
* else is a `409`; a replayed proof nonce is a `403`.
|
|
233
|
+
*/
|
|
234
|
+
claimNamespace(namespaceId: string, ownershipProof: unknown): Promise<void>;
|
|
235
|
+
/**
|
|
236
|
+
* Ask the fleet to host a claimed namespace — the step that gets a relay
|
|
237
|
+
* assigned.
|
|
238
|
+
*
|
|
239
|
+
* Takes no body: {@link claimNamespace} already established ownership, and
|
|
240
|
+
* this is the separate "now host it" half. Idempotent.
|
|
241
|
+
*
|
|
242
|
+
* Not to be confused with {@link enableHA}, which opens the cloud's web flow
|
|
243
|
+
* in a browser tab for a user to complete by hand. This is the API call.
|
|
244
|
+
*/
|
|
245
|
+
enableNamespaceHa(namespaceId: string): Promise<unknown>;
|
|
246
|
+
/**
|
|
247
|
+
* Stop hosting a claimed namespace.
|
|
248
|
+
*
|
|
249
|
+
* The fleet's own reconcile loop does the rest: each assigned node notices
|
|
250
|
+
* the namespace is no longer assigned to it and **self-leaves**, which makes
|
|
251
|
+
* core evict it and purge its local data and keys. So this is not just a
|
|
252
|
+
* billing flip.
|
|
253
|
+
*/
|
|
254
|
+
disableNamespaceHa(namespaceId: string): Promise<unknown>;
|
|
255
|
+
/**
|
|
256
|
+
* The node assigned to the signed-in user, or `null`.
|
|
257
|
+
*
|
|
258
|
+
* @deprecated Use {@link getMyMachines}. This reads the cloud's retired
|
|
259
|
+
* `NodeAssignment` ledger, which nothing has written since the v1
|
|
260
|
+
* retirement, so it answers `null` for every namespace-native account —
|
|
261
|
+
* which reads as "you have no machine" when the truth is "that is no longer
|
|
262
|
+
* how a machine is assigned".
|
|
263
|
+
*/
|
|
264
|
+
getMyNode(): Promise<Record<string, unknown> | null>;
|
|
17
265
|
enableHA(options: EnableHAOptions): void;
|
|
18
266
|
disableHA(options: DisableHAOptions): void;
|
|
267
|
+
/** Record a freshly-issued session and tell the app to persist it. */
|
|
268
|
+
private adoptSession;
|
|
269
|
+
private notifySession;
|
|
270
|
+
private request;
|
|
271
|
+
/**
|
|
272
|
+
* Adopt a session token the server refreshed mid-request.
|
|
273
|
+
*
|
|
274
|
+
* MDMA rotates the token on activity and returns the replacement in a
|
|
275
|
+
* response header rather than making the client ask. Ignoring it costs
|
|
276
|
+
* nothing immediately and then logs the user out a week later, which is the
|
|
277
|
+
* kind of bug that is impossible to reproduce in a short session.
|
|
278
|
+
*
|
|
279
|
+
* The user identity is carried over: the header holds only a token, and the
|
|
280
|
+
* refresh is the same logical session as the sign-in that named the user.
|
|
281
|
+
*/
|
|
282
|
+
private adoptRollingRefresh;
|
|
19
283
|
}
|