@calimero-network/mero-js 18.2.0 → 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 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
- await this.httpClient.post(`/admin-api/contexts/sync/${contextId ?? ''}`, {});
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
@@ -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..=8
5
- * (the entries below); bits 9 and above are unassigned — do not repurpose
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];
@@ -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..=8; bits 9 and above are unassigned
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..=8
11
- * (the entries below); bits 9 and above are unassigned — do not repurpose
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
  }