@calimero-network/mero-js 26.11.0 → 27.0.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.
Files changed (220) hide show
  1. package/README.md +73 -288
  2. package/dist/cloud-v2/attestation.d.ts +91 -0
  3. package/dist/cloud-v2/attestation.js +135 -0
  4. package/dist/cloud-v2/capabilities.d.ts +30 -0
  5. package/dist/cloud-v2/capabilities.js +31 -0
  6. package/dist/cloud-v2/client.d.ts +426 -0
  7. package/dist/cloud-v2/client.js +646 -0
  8. package/dist/cloud-v2/errors.d.ts +98 -0
  9. package/dist/cloud-v2/errors.js +165 -0
  10. package/dist/cloud-v2/index.d.ts +10 -0
  11. package/dist/cloud-v2/index.js +5 -0
  12. package/dist/cloud-v2/passkey.d.ts +27 -0
  13. package/dist/cloud-v2/passkey.js +73 -0
  14. package/dist/crypto/internal.d.ts +3 -37
  15. package/dist/crypto/internal.js +3 -72
  16. package/dist/index.browser.mjs +4 -2
  17. package/dist/index.browser.mjs.map +4 -4
  18. package/dist/index.cjs +1750 -12772
  19. package/dist/index.cjs.map +4 -4
  20. package/dist/index.d.ts +3 -56
  21. package/dist/index.js +14 -75
  22. package/dist/index.mjs +1750 -12772
  23. package/dist/index.mjs.map +4 -4
  24. package/dist/sealed/index.d.ts +4 -4
  25. package/dist/sealed/index.js +2 -2
  26. package/dist/sealed/release.d.ts +21 -76
  27. package/dist/sealed/release.js +24 -108
  28. package/dist/sealed/sealed.d.ts +8 -56
  29. package/dist/sealed/sealed.js +11 -89
  30. package/dist/sealed/verify.d.ts +7 -13
  31. package/dist/sealed/verify.js +3 -7
  32. package/package.json +8 -13
  33. package/dist/account/account-admin.d.ts +0 -80
  34. package/dist/account/account-admin.js +0 -346
  35. package/dist/account/account.d.ts +0 -196
  36. package/dist/account/account.js +0 -269
  37. package/dist/account/application-id.d.ts +0 -50
  38. package/dist/account/application-id.js +0 -83
  39. package/dist/account/bootstrap-from-invitation.d.ts +0 -110
  40. package/dist/account/bootstrap-from-invitation.js +0 -80
  41. package/dist/account/create-context.d.ts +0 -103
  42. package/dist/account/create-context.js +0 -267
  43. package/dist/account/govern.d.ts +0 -24
  44. package/dist/account/govern.js +0 -50
  45. package/dist/account/index.d.ts +0 -26
  46. package/dist/account/index.js +0 -15
  47. package/dist/account/join-as-account.d.ts +0 -23
  48. package/dist/account/join-as-account.js +0 -34
  49. package/dist/account/join-with-node.d.ts +0 -115
  50. package/dist/account/join-with-node.js +0 -161
  51. package/dist/account/node-admin.d.ts +0 -20
  52. package/dist/account/node-admin.js +0 -33
  53. package/dist/account/relay-from-invitation.d.ts +0 -137
  54. package/dist/account/relay-from-invitation.js +0 -180
  55. package/dist/account/relay-login-error.d.ts +0 -32
  56. package/dist/account/relay-login-error.js +0 -34
  57. package/dist/account/session.d.ts +0 -266
  58. package/dist/account/session.js +0 -915
  59. package/dist/account/wordlist.d.ts +0 -16
  60. package/dist/account/wordlist.js +0 -273
  61. package/dist/admin-api/admin-client.d.ts +0 -688
  62. package/dist/admin-api/admin-client.js +0 -1230
  63. package/dist/admin-api/admin-factory.d.ts +0 -14
  64. package/dist/admin-api/admin-factory.js +0 -22
  65. package/dist/admin-api/admin-types.d.ts +0 -2046
  66. package/dist/admin-api/admin-types.js +0 -12
  67. package/dist/admin-api/index.d.ts +0 -4
  68. package/dist/admin-api/index.js +0 -5
  69. package/dist/admin-api/redeem-invitation.d.ts +0 -101
  70. package/dist/admin-api/redeem-invitation.js +0 -200
  71. package/dist/auth/index.d.ts +0 -34
  72. package/dist/auth/index.js +0 -67
  73. package/dist/auth-api/auth-client.d.ts +0 -31
  74. package/dist/auth-api/auth-client.js +0 -117
  75. package/dist/auth-api/auth-factory.d.ts +0 -7
  76. package/dist/auth-api/auth-factory.js +0 -41
  77. package/dist/auth-api/auth-types.d.ts +0 -130
  78. package/dist/auth-api/auth-types.js +0 -2
  79. package/dist/auth-api/index.d.ts +0 -3
  80. package/dist/auth-api/index.js +0 -4
  81. package/dist/capabilities.d.ts +0 -84
  82. package/dist/capabilities.js +0 -117
  83. package/dist/cloud/account-ownership.d.ts +0 -63
  84. package/dist/cloud/account-ownership.js +0 -87
  85. package/dist/cloud/cloud-client.d.ts +0 -949
  86. package/dist/cloud/cloud-client.js +0 -1068
  87. package/dist/cloud/connect.d.ts +0 -213
  88. package/dist/cloud/connect.js +0 -260
  89. package/dist/cloud/enrol-redirect.d.ts +0 -181
  90. package/dist/cloud/enrol-redirect.js +0 -244
  91. package/dist/cloud/index.d.ts +0 -12
  92. package/dist/cloud/index.js +0 -10
  93. package/dist/cloud/link-redirect.d.ts +0 -95
  94. package/dist/cloud/link-redirect.js +0 -121
  95. package/dist/cloud/link-state.d.ts +0 -36
  96. package/dist/cloud/link-state.js +0 -104
  97. package/dist/cloud/routing-proof.d.ts +0 -113
  98. package/dist/cloud/routing-proof.js +0 -96
  99. package/dist/device-cert/device-cert.d.ts +0 -189
  100. package/dist/device-cert/device-cert.js +0 -239
  101. package/dist/device-cert/device-scope.d.ts +0 -58
  102. package/dist/device-cert/device-scope.js +0 -76
  103. package/dist/device-cert/index.d.ts +0 -4
  104. package/dist/device-cert/index.js +0 -3
  105. package/dist/ephemeral/codec.d.ts +0 -4
  106. package/dist/ephemeral/codec.js +0 -12
  107. package/dist/ephemeral/index.d.ts +0 -57
  108. package/dist/ephemeral/index.js +0 -101
  109. package/dist/ephemeral/types.d.ts +0 -27
  110. package/dist/ephemeral/types.js +0 -1
  111. package/dist/errors/classify.d.ts +0 -65
  112. package/dist/errors/classify.js +0 -117
  113. package/dist/errors/index.d.ts +0 -2
  114. package/dist/errors/index.js +0 -1
  115. package/dist/events/group.d.ts +0 -95
  116. package/dist/events/group.js +0 -10
  117. package/dist/events/index.d.ts +0 -5
  118. package/dist/events/index.js +0 -2
  119. package/dist/events/sse.d.ts +0 -127
  120. package/dist/events/sse.js +0 -469
  121. package/dist/events/ws.d.ts +0 -57
  122. package/dist/events/ws.js +0 -212
  123. package/dist/http-client/api-response.d.ts +0 -15
  124. package/dist/http-client/api-response.js +0 -1
  125. package/dist/http-client/http-factory.d.ts +0 -71
  126. package/dist/http-client/http-factory.js +0 -68
  127. package/dist/http-client/http-types.d.ts +0 -87
  128. package/dist/http-client/http-types.js +0 -1
  129. package/dist/http-client/index.d.ts +0 -7
  130. package/dist/http-client/index.js +0 -11
  131. package/dist/http-client/path-segment.d.ts +0 -1
  132. package/dist/http-client/path-segment.js +0 -8
  133. package/dist/http-client/retry.d.ts +0 -5
  134. package/dist/http-client/retry.js +0 -82
  135. package/dist/http-client/signal-utils.d.ts +0 -2
  136. package/dist/http-client/signal-utils.js +0 -41
  137. package/dist/http-client/web-client.d.ts +0 -80
  138. package/dist/http-client/web-client.js +0 -557
  139. package/dist/invitation/index.d.ts +0 -2
  140. package/dist/invitation/index.js +0 -2
  141. package/dist/invitation/invitation.d.ts +0 -106
  142. package/dist/invitation/invitation.js +0 -104
  143. package/dist/login/index.d.ts +0 -4
  144. package/dist/login/index.js +0 -3
  145. package/dist/login/login.d.ts +0 -111
  146. package/dist/login/login.js +0 -110
  147. package/dist/login/session.d.ts +0 -124
  148. package/dist/login/session.js +0 -141
  149. package/dist/mero-js.d.ts +0 -231
  150. package/dist/mero-js.js +0 -469
  151. package/dist/namespace-op/index.d.ts +0 -2
  152. package/dist/namespace-op/index.js +0 -3
  153. package/dist/namespace-op/namespace-op.d.ts +0 -150
  154. package/dist/namespace-op/namespace-op.js +0 -191
  155. package/dist/nodeDiscovery.d.ts +0 -47
  156. package/dist/nodeDiscovery.js +0 -101
  157. package/dist/owner-op/index.d.ts +0 -2
  158. package/dist/owner-op/index.js +0 -3
  159. package/dist/owner-op/owner-op.d.ts +0 -119
  160. package/dist/owner-op/owner-op.js +0 -214
  161. package/dist/presence/relay-presence.d.ts +0 -56
  162. package/dist/presence/relay-presence.js +0 -107
  163. package/dist/presence/statement.d.ts +0 -12
  164. package/dist/presence/statement.js +0 -17
  165. package/dist/relay/index.d.ts +0 -4
  166. package/dist/relay/index.js +0 -2
  167. package/dist/relay/nonce-source.d.ts +0 -72
  168. package/dist/relay/nonce-source.js +0 -111
  169. package/dist/relay/relay-client.d.ts +0 -472
  170. package/dist/relay/relay-client.js +0 -518
  171. package/dist/relay-attestation/attest-node-key.d.ts +0 -66
  172. package/dist/relay-attestation/attest-node-key.js +0 -110
  173. package/dist/relay-attestation/index.d.ts +0 -1
  174. package/dist/relay-attestation/index.js +0 -1
  175. package/dist/request/caller-proof.d.ts +0 -43
  176. package/dist/request/caller-proof.js +0 -36
  177. package/dist/request/index.d.ts +0 -3
  178. package/dist/request/index.js +0 -3
  179. package/dist/request/proof-signer.d.ts +0 -49
  180. package/dist/request/proof-signer.js +0 -59
  181. package/dist/request/request-sig.d.ts +0 -99
  182. package/dist/request/request-sig.js +0 -95
  183. package/dist/request-proof/authorizer.d.ts +0 -41
  184. package/dist/request-proof/authorizer.js +0 -60
  185. package/dist/request-proof/index.d.ts +0 -4
  186. package/dist/request-proof/index.js +0 -2
  187. package/dist/request-proof/request-proof.d.ts +0 -53
  188. package/dist/request-proof/request-proof.js +0 -95
  189. package/dist/rpc/index.d.ts +0 -64
  190. package/dist/rpc/index.js +0 -78
  191. package/dist/signer/index.d.ts +0 -2
  192. package/dist/signer/index.js +0 -3
  193. package/dist/signer/signer.d.ts +0 -92
  194. package/dist/signer/signer.js +0 -147
  195. package/dist/token-store/index.d.ts +0 -27
  196. package/dist/token-store/index.js +0 -62
  197. package/dist/transport/index.d.ts +0 -6
  198. package/dist/transport/index.js +0 -3
  199. package/dist/transport/mero-client.d.ts +0 -294
  200. package/dist/transport/mero-client.js +0 -386
  201. package/dist/transport/relay-observer.d.ts +0 -152
  202. package/dist/transport/relay-observer.js +0 -173
  203. package/dist/transport/relay-transport.d.ts +0 -73
  204. package/dist/transport/relay-transport.js +0 -94
  205. package/dist/transport/types.d.ts +0 -100
  206. package/dist/transport/types.js +0 -32
  207. package/dist/types/api-response.d.ts +0 -15
  208. package/dist/types/api-response.js +0 -1
  209. package/dist/types/index.d.ts +0 -1
  210. package/dist/types/index.js +0 -1
  211. package/dist/warrant/creation-warrant.d.ts +0 -163
  212. package/dist/warrant/creation-warrant.js +0 -194
  213. package/dist/warrant/governance-op.d.ts +0 -202
  214. package/dist/warrant/governance-op.js +0 -355
  215. package/dist/warrant/governance-warrant.d.ts +0 -124
  216. package/dist/warrant/governance-warrant.js +0 -164
  217. package/dist/warrant/index.d.ts +0 -8
  218. package/dist/warrant/index.js +0 -5
  219. package/dist/warrant/warrant.d.ts +0 -188
  220. package/dist/warrant/warrant.js +0 -202
@@ -1,1230 +0,0 @@
1
- import { withRetry } from '../http-client/index.js';
2
- import { pathSegment } from '../http-client/path-segment.js';
3
- import { CAPABILITIES, hasCap, withCap } from '../capabilities.js';
4
- import { redeemInvitation } from './redeem-invitation.js';
5
- /**
6
- * Helper: server wraps most responses in `{ data: T }`.
7
- * This extracts `.data` so callers get the inner payload directly. Null-safe so an
8
- * empty 2xx body (parsed as null) yields undefined instead of throwing.
9
- */
10
- function unwrap(response) {
11
- return response?.data;
12
- }
13
- /**
14
- * Compare two version strings by SemVer 2.0 precedence, ascending: negative if
15
- * `a < b`, positive if `a > b`, `0` if equal. Build metadata (`+...`) is stripped
16
- * and ignored. The numeric `major.minor.patch` core is compared field-by-field
17
- * (numerically when both parse, so `1.10.0 > 1.9.0`; a missing field is `0`). A
18
- * version WITH a pre-release ranks below the same core without one, and
19
- * pre-release identifiers compare per spec: dot-separated, numeric identifiers
20
- * numerically, alphanumeric lexically by ASCII, numeric below alphanumeric, and a
21
- * shorter set of identifiers loses when all preceding fields are equal.
22
- */
23
- export function compareSemver(a, b) {
24
- const [coreA, preA] = splitVersion(a);
25
- const [coreB, preB] = splitVersion(b);
26
- const na = coreA.split('.');
27
- const nb = coreB.split('.');
28
- const n = Math.max(na.length, nb.length);
29
- for (let i = 0; i < n; i++) {
30
- const c = compareSemverId(na[i] ?? '0', nb[i] ?? '0');
31
- if (c !== 0)
32
- return c;
33
- }
34
- // A pre-release ranks below the same core version with no pre-release.
35
- if (preA === undefined && preB === undefined)
36
- return 0;
37
- if (preA === undefined)
38
- return 1;
39
- if (preB === undefined)
40
- return -1;
41
- const pa = preA.split('.');
42
- const pb = preB.split('.');
43
- const m = Math.max(pa.length, pb.length);
44
- for (let i = 0; i < m; i++) {
45
- if (i >= pa.length)
46
- return -1; // shorter set loses when all preceding equal
47
- if (i >= pb.length)
48
- return 1;
49
- const c = compareSemverId(pa[i], pb[i]);
50
- if (c !== 0)
51
- return c;
52
- }
53
- return 0;
54
- }
55
- /** Drop build metadata and split the pre-release tail: `1.2.3-rc.1+b` -> `['1.2.3', 'rc.1']`. */
56
- function splitVersion(v) {
57
- const core = v.split('+', 1)[0];
58
- const dash = core.indexOf('-');
59
- return dash === -1 ? [core, undefined] : [core.slice(0, dash), core.slice(dash + 1)];
60
- }
61
- /**
62
- * Compare one identifier: numerically when both are all-digits, else lexically by
63
- * ASCII with a numeric identifier ranking below an alphanumeric one. Fallback: a
64
- * non-numeric core component (loose/malformed input) is treated as a string, so
65
- * comparison never throws.
66
- */
67
- function compareSemverId(a, b) {
68
- const na = /^\d+$/.test(a);
69
- const nb = /^\d+$/.test(b);
70
- if (na && nb) {
71
- const d = Number(a) - Number(b);
72
- return d < 0 ? -1 : d > 0 ? 1 : 0;
73
- }
74
- if (na)
75
- return -1;
76
- if (nb)
77
- return 1;
78
- return a < b ? -1 : a > b ? 1 : 0;
79
- }
80
- /**
81
- * Fetch a URL from the (foreign-origin) registry with a per-attempt timeout and
82
- * retry on transient failures. Deliberately NOT routed through the node
83
- * `HttpClient`: that client attaches the node's bearer token to every request,
84
- * which must never leak to the registry's origin. Each attempt times out after
85
- * ~10s; network errors and 5xx are retried (2 retries, short backoff via the
86
- * shared `withRetry`), while a 4xx is a definitive answer and is not retried.
87
- * `kind`/`context` shape the error message so failures stay debuggable in logs.
88
- */
89
- async function fetchRegistry(url, kind, context) {
90
- return withRetry(async () => {
91
- const resp = await fetch(url, { signal: AbortSignal.timeout(10000) });
92
- if (!resp.ok) {
93
- // Carry `status` so withRetry retries 5xx but leaves 4xx to throw.
94
- const err = new Error(`registry ${kind} fetch failed (${resp.status}) for ${context}`);
95
- err.status = resp.status;
96
- throw err;
97
- }
98
- return resp;
99
- }, { attempts: 3 });
100
- }
101
- export class AdminApiClient {
102
- constructor(httpClient) {
103
- this.httpClient = httpClient;
104
- }
105
- // ---- Health and Status (public, no auth) ----
106
- async healthCheck() {
107
- return unwrap(await this.httpClient.get('/admin-api/health'));
108
- }
109
- /**
110
- * Whether the node has finished starting and its store answers. Public, like
111
- * {@link healthCheck}; unlike it, a node that is up but not ready answers
112
- * `503`, which this surfaces as an `HTTPError` whose body names the stage.
113
- */
114
- async readinessCheck() {
115
- return unwrap(await this.httpClient.get('/admin-api/ready'));
116
- }
117
- async isAuthed() {
118
- return this.httpClient.get('/admin-api/is-authed');
119
- }
120
- // ---- Application Management ----
121
- /**
122
- * Install a published application by its `package@version` coordinates. The
123
- * node resolves and fetches them from its own configured `[registry]`, so no
124
- * URL is passed and none is accepted. A 502 means the node's source has
125
- * nothing published at those coordinates. This is the discrete "download"
126
- * step an Updates flow pairs with a subsequent `upgradeGroup`.
127
- */
128
- async installApplication(request) {
129
- return unwrap(await this.httpClient.post('/admin-api/install-application', request));
130
- }
131
- /**
132
- * List a package's published versions from the registry, newest-first by
133
- * semver. Reads the registry's V2 bundle listing
134
- * (`GET {registry}/api/v2/bundles?package={package}`), taking each bundle's
135
- * `appVersion`. Registry-side data — distinct from the node's
136
- * installed-version list — and the source an Updates view compares against
137
- * the running `Context.applicationVersion` to detect "a new version exists".
138
- */
139
- async getRegistryVersions(registryUrl, packageName) {
140
- const url = new URL('/api/v2/bundles', new URL(registryUrl).origin);
141
- url.searchParams.set('package', packageName);
142
- const resp = await fetchRegistry(url.toString(), 'versions', packageName);
143
- const bundles = (await resp.json());
144
- return (Array.isArray(bundles) ? bundles : [])
145
- .map((b) => b.appVersion)
146
- .filter((v) => typeof v === 'string')
147
- .sort((a, b) => compareSemver(b, a));
148
- }
149
- async installDevApplication(request) {
150
- return unwrap(await this.httpClient.post('/admin-api/install-dev-application', request));
151
- }
152
- async uninstallApplication(appId) {
153
- return unwrap(await this.httpClient.delete(`/admin-api/applications/${pathSegment(appId)}`));
154
- }
155
- async listApplications() {
156
- return unwrap(await this.httpClient.get('/admin-api/applications'));
157
- }
158
- async getApplication(appId) {
159
- return unwrap(await this.httpClient.get(`/admin-api/applications/${pathSegment(appId)}`));
160
- }
161
- /**
162
- * Installed-blob inventory for an application — one entry per locally installed
163
- * version. This is the *installed* inventory (source for a "pick a version to
164
- * pin" UI); the registry equivalent is `listPackageVersions`.
165
- */
166
- async listApplicationVersions(applicationId) {
167
- return unwrap(await this.httpClient.get(`/admin-api/applications/${pathSegment(applicationId)}/versions`));
168
- }
169
- /**
170
- * The application's embedded `wasm-abi/1` manifest. Omit `serviceName` for
171
- * single-service apps; core 400s if the ABI is absent or the name is ambiguous.
172
- */
173
- async getApplicationAbi(applicationId, serviceName) {
174
- const query = serviceName ? `?service_name=${encodeURIComponent(serviceName)}` : '';
175
- return unwrap(await this.httpClient.get(`/admin-api/applications/${pathSegment(applicationId)}/abi${query}`));
176
- }
177
- // ---- Package Management ----
178
- async listPackages() {
179
- // Core returns this flat ({ packages: [...] }), not under `data`; tolerate both.
180
- const r = await this.httpClient.get('/admin-api/packages');
181
- return (r?.data ?? r);
182
- }
183
- async listPackageVersions(packageName) {
184
- // Core returns this flat ({ versions: [...] }), not under `data`; tolerate both.
185
- const r = await this.httpClient.get(`/admin-api/packages/${pathSegment(packageName)}/versions`);
186
- return (r?.data ?? r);
187
- }
188
- async getLatestPackageVersion(packageName) {
189
- return this.httpClient.get(`/admin-api/packages/${pathSegment(packageName)}/latest`);
190
- }
191
- // ---- Context Management ----
192
- async createContext(request) {
193
- // Core requires `initializationParams` (no default); default it to an empty
194
- // byte array so callers that pass none don't get a 400.
195
- const body = { ...request, initializationParams: request.initializationParams ?? [] };
196
- return unwrap(await this.httpClient.post('/admin-api/contexts', body));
197
- }
198
- async deleteContext(contextId) {
199
- return unwrap(await this.httpClient.delete(`/admin-api/contexts/${pathSegment(contextId)}`));
200
- }
201
- async getContexts() {
202
- return unwrap(await this.httpClient.get('/admin-api/contexts'));
203
- }
204
- async getContext(contextId) {
205
- return unwrap(await this.httpClient.get(`/admin-api/contexts/${pathSegment(contextId)}`));
206
- }
207
- async getContextsForApplication(applicationId) {
208
- return unwrap(await this.httpClient.get(`/admin-api/contexts/for-application/${pathSegment(applicationId)}`));
209
- }
210
- // ---- Context Identity ----
211
- async generateContextIdentity() {
212
- return unwrap(await this.httpClient.post('/admin-api/identity/context', {}));
213
- }
214
- async getContextIdentities(contextId) {
215
- return unwrap(await this.httpClient.get(`/admin-api/contexts/${pathSegment(contextId)}/identities`));
216
- }
217
- async getContextIdentitiesOwned(contextId) {
218
- return unwrap(await this.httpClient.get(`/admin-api/contexts/${pathSegment(contextId)}/identities-owned`));
219
- }
220
- // ---- Context join (group membership) ----
221
- async joinContext(contextId) {
222
- return unwrap(await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/join`, {}));
223
- }
224
- // ---- Context group / storage / sync ----
225
- async getContextGroup(contextId) {
226
- return unwrap(await this.httpClient.get(`/admin-api/contexts/${pathSegment(contextId)}/group`));
227
- }
228
- async getContextStorage(contextId) {
229
- return unwrap(await this.httpClient.get(`/admin-api/contexts/${pathSegment(contextId)}/storage`));
230
- }
231
- /** Sync one context, or every context when `contextId` is omitted. */
232
- async syncContext(contextId) {
233
- // Sync-all is `/contexts/sync` with no trailing slash - core routes the two
234
- // spellings separately and 404s the trailing one.
235
- const path = contextId ? `/admin-api/contexts/sync/${pathSegment(contextId)}` : '/admin-api/contexts/sync';
236
- await this.httpClient.post(path, {});
237
- }
238
- /**
239
- * Kick off a full state re-pull for a context (operator recovery for a
240
- * stranded context). `force` re-pulls even when the node does not flag the
241
- * context as stranded.
242
- */
243
- async resyncContext(contextId, request = {}) {
244
- // Core's `ResyncContextApiResponse` is a flat payload (no inner `data`
245
- // field), so parse the body directly — do NOT `unwrap`, or `resyncStarted`
246
- // reads as undefined and the resync silently appears to never start.
247
- const r = await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/resync`, request);
248
- // An empty 2xx body means the resync was accepted; synthesize the result so
249
- // callers always get a typed value instead of null.
250
- return r ?? { contextId, resyncStarted: true };
251
- }
252
- async updateContextApplication(contextId, request) {
253
- await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/application`, request);
254
- }
255
- async getContextsWithExecutorsForApplication(applicationId) {
256
- // Core returns this flat as { contexts: [...] } (not a bare array under `data`).
257
- const r = await this.httpClient.get(`/admin-api/contexts/with-executors/for-application/${pathSegment(applicationId)}`);
258
- if (Array.isArray(r))
259
- return r;
260
- return (r?.contexts ??
261
- r?.data ??
262
- []);
263
- }
264
- // ---- Blob Management ----
265
- async uploadBlob(request) {
266
- // Core streams the raw request body into blob storage (no JSON) and takes
267
- // its params from the query string (`hash`, `context_id` — snake_case).
268
- const params = new URLSearchParams();
269
- if (request.hash)
270
- params.set('hash', request.hash);
271
- if (request.contextId)
272
- params.set('context_id', request.contextId);
273
- const query = params.toString();
274
- const path = query ? `/admin-api/blobs?${query}` : '/admin-api/blobs';
275
- // request.data (Uint8Array view | ArrayBuffer | Blob) is a valid BodyInit and
276
- // is streamed verbatim — no JSON, no cast. fetch honors a Uint8Array view's
277
- // byteOffset/byteLength, so subarrays upload the correct region.
278
- // Core's BlobInfo is snake_case (`blob_id`); map to camelCase like deleteBlob.
279
- const res = unwrap(await this.httpClient.request(path, {
280
- method: 'PUT',
281
- body: request.data,
282
- headers: { 'Content-Type': 'application/octet-stream' },
283
- }));
284
- return { blobId: res.blob_id, size: res.size };
285
- }
286
- async deleteBlob(blobId) {
287
- // Core's `BlobDeleteResponse` is a flat, snake_case payload (`{ blob_id,
288
- // deleted }`) — the lone admin DTO without a camelCase rename and without an
289
- // inner `data` field. Parse directly (no `unwrap`) and map to camelCase.
290
- const body = await this.httpClient.delete(`/admin-api/blobs/${pathSegment(blobId)}`);
291
- return { blobId: body.blob_id, deleted: body.deleted };
292
- }
293
- async listBlobs() {
294
- // Core's BlobInfo is snake_case (`blob_id`); map to camelCase.
295
- const res = unwrap(await this.httpClient.get('/admin-api/blobs'));
296
- return { blobs: res.blobs.map((b) => ({ blobId: b.blob_id, size: b.size })) };
297
- }
298
- /**
299
- * Blob-read query params. `context_id` is the same snake_case param
300
- * {@link uploadBlob} announces with; building it in one place keeps the read
301
- * side from drifting from the write side.
302
- */
303
- blobPath(blobId, options) {
304
- if (!options?.contextId)
305
- return `/admin-api/blobs/${pathSegment(blobId)}`;
306
- const params = new URLSearchParams({ context_id: options.contextId });
307
- return `/admin-api/blobs/${pathSegment(blobId)}?${params.toString()}`;
308
- }
309
- /**
310
- * Download a blob's raw bytes. `GET /admin-api/blobs/:id` streams the blob
311
- * content (e.g. `application/gzip`), NOT JSON — so fetch it as an ArrayBuffer.
312
- * Use {@link listBlobs} for `{ blobId, size }` metadata.
313
- *
314
- * Without `contextId` the read is local-only: the node answers from its own
315
- * blob store and returns immediately, or 404s. Passing `contextId` opts into
316
- * network discovery — the node probes that context's peers (availability
317
- * nodes first) for a holder, then transfers the bytes. Budget for it: core
318
- * bounds discovery by a ~30s deadline, and the transfer is on top of that, so
319
- * the worst case is well past what a UI should block on unhinted.
320
- */
321
- async getBlob(blobId, options) {
322
- return this.httpClient.get(this.blobPath(blobId, options), {
323
- parse: 'arrayBuffer',
324
- });
325
- }
326
- /**
327
- * Fetch a blob's metadata without downloading it. `HEAD /admin-api/blobs/:id`
328
- * returns the info in response headers (size via `content-length`, plus
329
- * `x-blob-id`/`x-blob-hash`/`x-blob-mime-type`).
330
- *
331
- * Without `contextId` this is a local-only presence check that returns
332
- * immediately. Passing `contextId` opts into the same network discovery
333
- * {@link getBlob} uses — presence and size answered by probing the context's
334
- * peers, with no transfer — so it inherits core's ~30s discovery deadline as
335
- * its worst case. A probe only carries `found` and `size`, so a peer-sourced
336
- * answer has no `hash`/`mimeType`; read `source` to tell the two apart.
337
- */
338
- async getBlobInfo(blobId, options) {
339
- // HEAD throws (HttpError) on a non-2xx status, so we only reach here on success
340
- // — the x-blob-* headers are present. Guard size against a missing/non-numeric
341
- // content-length anyway (defaults to 0 rather than NaN).
342
- const { headers } = await this.httpClient.head(this.blobPath(blobId, options));
343
- const size = Number(headers['content-length']);
344
- return {
345
- blobId: headers['x-blob-id'] ?? blobId,
346
- size: Number.isFinite(size) ? size : 0,
347
- hash: headers['x-blob-hash'],
348
- mimeType: headers['x-blob-mime-type'],
349
- // Nodes that predate the context-aware HEAD omit this header entirely, so
350
- // it stays undefined rather than being defaulted to 'local' — absent means
351
- // "the node didn't say", which is not the same claim as "served locally".
352
- source: headers['x-blob-source'],
353
- };
354
- }
355
- // ---- Alias Management ----
356
- async createContextAlias(request) {
357
- return unwrap(await this.httpClient.post('/admin-api/alias/create/context', { alias: request.alias, contextId: request.contextId }));
358
- }
359
- async createApplicationAlias(request) {
360
- return unwrap(await this.httpClient.post('/admin-api/alias/create/application', { alias: request.alias, applicationId: request.applicationId }));
361
- }
362
- async lookupContextAlias(name) {
363
- return unwrap(await this.httpClient.post(`/admin-api/alias/lookup/context/${pathSegment(name)}`, {}));
364
- }
365
- async lookupApplicationAlias(name) {
366
- return unwrap(await this.httpClient.post(`/admin-api/alias/lookup/application/${pathSegment(name)}`, {}));
367
- }
368
- async deleteContextAlias(name) {
369
- return unwrap(await this.httpClient.post(`/admin-api/alias/delete/context/${pathSegment(name)}`, {}));
370
- }
371
- async deleteApplicationAlias(name) {
372
- return unwrap(await this.httpClient.post(`/admin-api/alias/delete/application/${pathSegment(name)}`, {}));
373
- }
374
- async listContextAliases() {
375
- return unwrap(await this.httpClient.get('/admin-api/alias/list/context'));
376
- }
377
- async listApplicationAliases() {
378
- return unwrap(await this.httpClient.get('/admin-api/alias/list/application'));
379
- }
380
- async createDeviceAlias(request) {
381
- return unwrap(await this.httpClient.post('/admin-api/alias/create/device', { alias: request.alias, deviceId: request.deviceId }));
382
- }
383
- async lookupDeviceAlias(name) {
384
- return unwrap(await this.httpClient.post(`/admin-api/alias/lookup/device/${pathSegment(name)}`, {}));
385
- }
386
- async deleteDeviceAlias(name) {
387
- return unwrap(await this.httpClient.post(`/admin-api/alias/delete/device/${pathSegment(name)}`, {}));
388
- }
389
- async listDeviceAliases() {
390
- return unwrap(await this.httpClient.get('/admin-api/alias/list/device'));
391
- }
392
- // ---- Namespace Management ----
393
- async listNamespaces() {
394
- return unwrap(await this.httpClient.get('/admin-api/namespaces'));
395
- }
396
- async getNamespace(namespaceId) {
397
- return unwrap(await this.httpClient.get(`/admin-api/namespaces/${pathSegment(namespaceId)}`));
398
- }
399
- /**
400
- * Who this node is — the account it writes as, the device it is, the key it
401
- * signs with, and the account root a second device pairs against.
402
- *
403
- * Takes no namespace, and that is the point: one root key is one account
404
- * everywhere, and a node signs with one key, so none of it varies by
405
- * namespace.
406
- */
407
- async getNodeIdentity() {
408
- return unwrap(await this.httpClient.get('/admin-api/identity'));
409
- }
410
- /**
411
- * Ask this node to run one method on a member's behalf, under a warrant that
412
- * member signed.
413
- *
414
- * The node executes and signs the envelope with its own key; the change is
415
- * attributed to the **author**. Both halves travel together, so every peer
416
- * re-checks that the member consented instead of trusting the relay.
417
- *
418
- * Three things have to be true first, and none is implied by the others:
419
- *
420
- * - the author's **account** is a member of the group owning the context —
421
- * their device joins nothing and is in no group's binding rows, which is
422
- * exactly the case a device certificate covers;
423
- * - this node holds `CAN_AUTHOR_ON_BEHALF` on that group, which is implied by
424
- * neither membership nor admin;
425
- * - the warrant has not been spent. It is single-use: presenting a valid one
426
- * twice is refused, because the signature stays valid forever and replay is
427
- * not forgery.
428
- *
429
- * A refusal comes back as a `403` carrying the reason — "an admin must grant
430
- * CAN_AUTHOR_ON_BEHALF to …", or that the nonce is already spent — so a caller
431
- * can tell which precondition failed rather than only that one did.
432
- *
433
- * Mint the warrant with {@link signWarrant}; see
434
- * {@link PerformIntentRequest.warrant}.
435
- */
436
- async performIntent(contextId, request) {
437
- return unwrap(await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/intents`, request));
438
- }
439
- /**
440
- * Read a context as the session's ACCOUNT, without minting a warrant.
441
- *
442
- * Needs an account-authenticated session — one from {@link login}, not the
443
- * admin password login — and refuses otherwise with a `401`. The account must
444
- * be a member of the group owning the context (`403` if not), and the method
445
- * must be declared read-only in the app's ABI (`409` if not; a write needs
446
- * {@link performIntent}).
447
- */
448
- async queryContext(contextId, request) {
449
- return unwrap(await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/query`, request));
450
- }
451
- /**
452
- * Issue an access grant: a token whose holder may call `request.methods` in
453
- * `contextId`, as this node, with no session of its own.
454
- *
455
- * A grant only narrows: each method must be one this session's own token may
456
- * execute in the context (`403` otherwise), and an account session is refused
457
- * (`403`), since a granted call runs as the node. The token is in the answer
458
- * once and never again; the node keeps only its hash.
459
- */
460
- async createAccessGrant(contextId, request) {
461
- return unwrap(await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/grants`, { methods: request.methods, label: request.label ?? '', notAfter: request.notAfter ?? null }));
462
- }
463
- /** The access grants of `contextId` this session could have issued. */
464
- async listAccessGrants(contextId) {
465
- return unwrap(await this.httpClient.get(`/admin-api/contexts/${pathSegment(contextId)}/grants`));
466
- }
467
- /** Revoke an access grant. A grant this session cannot see is a `404`. */
468
- async revokeAccessGrant(contextId, grantId) {
469
- return unwrap(await this.httpClient.delete(`/admin-api/contexts/${pathSegment(contextId)}/grants/${pathSegment(grantId)}`));
470
- }
471
- /**
472
- * Call one method under an access grant. The token is the whole credential:
473
- * the route is public on every node, so this works from a client holding no
474
- * session at all. A method the grant does not name, an unknown or revoked
475
- * token, and an expired grant are each a `403`.
476
- */
477
- async callWithAccessGrant(contextId, request) {
478
- return unwrap(await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/grant-calls`, request));
479
- }
480
- /**
481
- * Ask what this node can do for a member in `contextId`, before they sign
482
- * anything.
483
- *
484
- * The facts {@link performIntent} needs and a client cannot derive: whose
485
- * account and key go in the warrant's `executor` and `executor_key`, which
486
- * release it pins, and whether this node holds `CAN_AUTHOR_ON_BEHALF` on the
487
- * owning group.
488
- * `canAuthorOnBehalf: false` comes back as an answer rather than an error, as
489
- * it is the default state of every context, so a caller can say "an admin of
490
- * `groupId` must grant it to `executorAccount`" instead of presenting a
491
- * warrant that will be refused after the author has spent a nonce on it.
492
- *
493
- * `404` means the context belongs to no group, this node holds no account or
494
- * identity in it yet, or its group names no release yet; no warrant can be
495
- * spent here until that changes.
496
- *
497
- * For a caller with no credential on the node at all — a browser tab holding
498
- * one signing key — use `RelayClient`, which reads this same route on a node
499
- * run as a relay.
500
- */
501
- async getIntentRelay(contextId) {
502
- return unwrap(await this.httpClient.get(`/admin-api/contexts/${pathSegment(contextId)}/intents`));
503
- }
504
- async listNamespacesForApplication(applicationId) {
505
- return unwrap(await this.httpClient.get(`/admin-api/namespaces/for-application/${pathSegment(applicationId)}`));
506
- }
507
- async createNamespace(request) {
508
- return unwrap(await this.httpClient.post('/admin-api/namespaces', request));
509
- }
510
- /** What `namespaceId` approves for outbound requests, one entry per publisher. */
511
- async listOutboundApprovals(namespaceId) {
512
- return unwrap(await this.httpClient.get(`/admin-api/namespaces/${pathSegment(namespaceId)}/outbound-approvals`));
513
- }
514
- /**
515
- * Set what `namespaceId` approves for the publisher of `request.applicationId`
516
- * to exactly `request.approve`: `'manifest'` or a list of URL prefixes, and
517
- * `[]` revokes everything. Takes effect at the app's next run.
518
- */
519
- async setOutboundApproval(namespaceId, request) {
520
- return unwrap(await this.httpClient.put(`/admin-api/namespaces/${pathSegment(namespaceId)}/outbound-approvals`,
521
- // Core declares this body `deny_unknown_fields`, so send exactly these.
522
- { applicationId: request.applicationId, approve: request.approve }));
523
- }
524
- async deleteNamespace(namespaceId) {
525
- // Core requires `Content-Type: application/json` on this DELETE even though
526
- // the body is always empty, so send the header and an empty JSON object.
527
- return unwrap(await this.httpClient.request(`/admin-api/namespaces/${pathSegment(namespaceId)}`, {
528
- method: 'DELETE',
529
- body: '{}',
530
- headers: { 'Content-Type': 'application/json' },
531
- }));
532
- }
533
- async createNamespaceInvitation(namespaceId, request) {
534
- return unwrap(await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/invite`, request ?? {}));
535
- }
536
- /**
537
- * Present a join this caller signed to a node the inviter named as an
538
- * admitter.
539
- *
540
- * For a member with no node. `joinNamespace` publishes the membership op from
541
- * the node making the call; this hands an already-signed op to somebody else's
542
- * node to publish. The distinction matters because a keyholder has nowhere to
543
- * publish from.
544
- *
545
- * The op must be signed by the **device key** in the credential it carries —
546
- * every peer checks that when applying a join. So the admitter cannot author
547
- * this, only carry it: a hostile one can refuse, and nothing else. It cannot
548
- * admit a different account, change the group, or grant a role.
549
- *
550
- * @param namespaceId the namespace being joined, 64 hex
551
- * @param request the invitation and the hex-encoded signed op
552
- */
553
- async admitJoin(namespaceId, request) {
554
- return unwrap(await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/admit`, request, { timeoutMs: 65000 }));
555
- }
556
- async joinNamespace(namespaceId, request) {
557
- const data = unwrap(await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/join`, request, { timeoutMs: 65000 }));
558
- // core 0.11.0-rc.25 renamed this response's `groupId` to `namespaceId`
559
- // (core#3598). Bind whichever spelling the node actually sent rather than
560
- // picking one: the rename lands as `undefined` on the field the client
561
- // reads, and nothing errors — no 4xx, no thrown parse — so a client that
562
- // knows only one spelling fails much later and somewhere else. Normalising
563
- // here means one mero-js works against nodes either side of that release.
564
- if (data && data.namespaceId === undefined && data.groupId !== undefined) {
565
- return { ...data, namespaceId: data.groupId };
566
- }
567
- return data;
568
- }
569
- /**
570
- * Bring back a namespace the user removed from this node while the account
571
- * stayed a member (POST /admin-api/namespaces/:namespace_id/join, no
572
- * invitation). Such namespaces are the `forgotten` ones in
573
- * {@link listNamespacesForApplication}.
574
- */
575
- async restoreNamespace(namespaceId) {
576
- return unwrap(await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/join`, {}, { timeoutMs: 65000 }));
577
- }
578
- /**
579
- * Join the namespace an invitation grants, and say what happened — including
580
- * when the request failed but the join landed anyway.
581
- *
582
- * Use this rather than {@link joinNamespace} whenever someone is following an
583
- * invitation. A join waits for a member to come online and can outlast any
584
- * proxy in front of the node; the request then fails while the join
585
- * succeeds. This sends the join once, then checks {@link listNamespaces}:
586
- * a failed request whose namespace is listed is `already-member`, a success.
587
- *
588
- * Never throws for an ordinary failure. A failed outcome carries a `reason`
589
- * read off the node's status (core rc.56+), and `retryable`, which says
590
- * whether to keep the invitation for another attempt.
591
- *
592
- * @param namespaceId the namespace the invitation names, 64 hex
593
- * @param invitation the signed invitation, as the inviter's node minted it
594
- * @param options `groupName` is sent with the join; `teamName` is echoed back
595
- * on a successful outcome for the caller's UI
596
- */
597
- async redeemInvitation(namespaceId, invitation, options = {}) {
598
- const { groupName, teamName } = options;
599
- return redeemInvitation({ namespaceId, invitation, teamName }, {
600
- join: async () => {
601
- await this.joinNamespace(namespaceId, groupName === undefined ? { invitation } : { invitation, groupName });
602
- },
603
- memberships: async () => ((await this.listNamespaces()) ?? []).map((n) => n.namespaceId),
604
- });
605
- }
606
- async createGroupInNamespace(namespaceId, request) {
607
- return unwrap(await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/groups`, request ?? {}));
608
- }
609
- async listNamespaceGroups(namespaceId) {
610
- return unwrap(await this.httpClient.get(`/admin-api/namespaces/${pathSegment(namespaceId)}/groups`));
611
- }
612
- // ---- Account Devices & Pairing ----
613
- /**
614
- * Mint this node a device for an account that already lives somewhere else -
615
- * the first half of pairing, run on the joining node.
616
- *
617
- * Publishes nothing and needs no key: it produces the device id, the two
618
- * public keys and the signature that the account holder's
619
- * {@link completeAccountPairing} certifies, plus a code to read out to them
620
- * over a channel the payload does not travel on. Until that second half runs,
621
- * the device this mints is inert.
622
- *
623
- * Takes a set of namespaces rather than one, and takes it from the caller
624
- * because this node cannot discover it: a node that is a member of nothing can
625
- * neither read the account's namespace set off a DAG nor derive it. One device
626
- * covers however many are named - the certificate is over the account, not a
627
- * scope - so there is one id and one code to hand over regardless.
628
- */
629
- async initAccountPairing(request) {
630
- return unwrap(await this.httpClient.post('/admin-api/account/pair-init', request));
631
- }
632
- /**
633
- * Certify a device another node minted, link it, and deliver its scope keys -
634
- * the second half of pairing, run on the node that holds the account root.
635
- *
636
- * Every field but `applications` is copied verbatim out of that node's
637
- * {@link initAccountPairing} answer. Only this side can complete it: the
638
- * certificate is signed by the account root, which never leaves the node that
639
- * holds it.
640
- *
641
- * The scope is named in applications, not namespaces, because that is the
642
- * question a person can answer - a namespace is an implementation unit they
643
- * never chose. Leave it out to grant every application.
644
- */
645
- async completeAccountPairing(request) {
646
- return unwrap(await this.httpClient.post('/admin-api/account/pair-complete', request));
647
- }
648
- /**
649
- * Re-run the pairing fan-out for a device this account already certified,
650
- * against the namespaces this node takes part in now.
651
- *
652
- * Pairing is a snapshot, and this is how it is repeated. A namespace created
653
- * or joined afterwards holds no binding for a device paired before it, and the
654
- * symptom is silence - the device simply never sees that namespace. Nothing
655
- * but the id is needed to repair it: the certificate is already held here,
656
- * root-signed and naming no namespace, so a fresh endorsement and a key wrap
657
- * are all a later namespace is missing. The device does not have to be online.
658
- *
659
- * Passing `applications` widens the device's stored scope as well as
660
- * repairing. Passing none - or an empty list - repairs against the scope
661
- * already stored, which is deliberately NOT the "every application" that an
662
- * empty list means on {@link completeAccountPairing}: overloading it here
663
- * would make the accidental request the widest one.
664
- */
665
- async relinkAccountDevice(deviceId, request) {
666
- return unwrap(await this.httpClient.post(`/admin-api/account/devices/${pathSegment(deviceId)}/relink`, request ?? {}));
667
- }
668
- /**
669
- * Replace a device's scope outright, in one root-signed op.
670
- *
671
- * {@link relinkAccountDevice} only ever adds applications; this is how a
672
- * scope is narrowed - or an `all` device is given a fixed list instead. The
673
- * device need not be online.
674
- */
675
- async rescopeAccountDevice(deviceId, request) {
676
- return unwrap(await this.httpClient.put(`/admin-api/account/devices/${pathSegment(deviceId)}/scope`, request));
677
- }
678
- /**
679
- * Sign a payload an outside verifier specified, with this account's root.
680
- *
681
- * The counterpart to a verifier that issued a challenge and will check the
682
- * signature against the account id it derives — the cloud's account link and
683
- * login exchanges being the ones this exists for. The root never leaves the
684
- * node, so this is the only way a client can produce that signature.
685
- *
686
- * `domain` is a NAME from a closed set the node knows, never the domain bytes.
687
- * That is the security property rather than an encoding preference: signing
688
- * caller-supplied bytes under a caller-supplied prefix would be an oracle over
689
- * the one key that can certify a device. A node refuses a name it does not
690
- * know, and refuses one whose bytes could be extended into a credential of its
691
- * own.
692
- *
693
- * What comes back proves possession of the root and nothing else. Whether the
694
- * payload is a challenge anyone issued, whether it is fresh, and whether the
695
- * holder may link this account are the verifier's to decide — it issued the
696
- * challenge and knows who to.
697
- *
698
- * ```ts
699
- * const { nonce } = await cloud.getAccountLoginChallenge();
700
- * const { rootPublicKey, signature } = await admin.signWithAccountRoot({
701
- * domain: 'mdma.account-login',
702
- * payload: hexEncodeUtf8(nonce),
703
- * });
704
- * await cloud.submitAccountLogin({ rootPublicKey, nonce, signature });
705
- * ```
706
- */
707
- async signWithAccountRoot(request) {
708
- return unwrap(await this.httpClient.post('/admin-api/account/sign-with-root', request));
709
- }
710
- /**
711
- * Name a device of this account, for a listing to render.
712
- *
713
- * Run on the node holding the account root to name any device; a paired
714
- * node is accepted only for that device's own id.
715
- */
716
- async labelAccountDevice(deviceId, request) {
717
- return unwrap(await this.httpClient.put(`/admin-api/account/devices/${pathSegment(deviceId)}/label`, request));
718
- }
719
- /**
720
- * Every device of this account, as this node sees them - its own included.
721
- *
722
- * Joined from the node-local certificate cache and the live bindings of every
723
- * namespace this node takes part in, so it is this node's view rather than a
724
- * global one: a device certified by another holder appears with an empty
725
- * `applications`, because the certificate that would name its scope is not
726
- * cached here.
727
- *
728
- * The wire wraps this in `devices` rather than the usual `data`; the array is
729
- * what a caller wants, so that wrapper is not passed on.
730
- */
731
- async listAccountDevices() {
732
- const response = await this.httpClient.get('/admin-api/account/devices');
733
- return response.devices;
734
- }
735
- /**
736
- * Every application this account speaks in, with the namespaces targeting each.
737
- *
738
- * What a device-scoping UI lists: {@link completeAccountPairing} and
739
- * {@link relinkAccountDevice} both take applications, and this is where the
740
- * ids they take come from. Derived from the namespaces this node takes part
741
- * in, so an application installed with no namespace yet is absent - it has
742
- * nothing to scope a device to until one exists.
743
- *
744
- * Wrapped in `applications` on the wire rather than `data`, unwrapped here for
745
- * the same reason {@link listAccountDevices} unwraps `devices`.
746
- */
747
- async listAccountApplications() {
748
- const response = await this.httpClient.get('/admin-api/account/applications');
749
- return response.applications;
750
- }
751
- /**
752
- * Withdraw a device from this account, terminally.
753
- *
754
- * Two authorities reach the same endpoint. An admin may revoke any device and
755
- * rotates the scope key as it goes. The account holder may revoke its own with
756
- * a root-signed `proof`, which does not rotate - so read
757
- * {@link RevokeAccountDeviceResponseData.revokedIn} rather than assuming the
758
- * device lost its access: a namespace reporting `keyRotated: false` has
759
- * stopped the device writing but leaves it able to read until an admin
760
- * rotates.
761
- *
762
- * `namespaceId` names where to publish from, not the extent of the revocation:
763
- * a device belongs to the account, so every namespace holding a binding for it
764
- * is withdrawn from and reported back.
765
- *
766
- * **Revocation is forward-only.** It withdraws the device's authority from
767
- * here on and does NOT retract past authorship: ops the device already signed
768
- * keep verifying and stay in the DAG, because the certificate that authorised
769
- * them was valid when the signature was made. Nothing in this system can
770
- * un-author what a device wrote, so a UI built on this call must not offer
771
- * that - "this device can no longer act" is the only claim it supports.
772
- */
773
- async revokeAccountDevice(namespaceId, request) {
774
- return unwrap(await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/account/revoke`, request));
775
- }
776
- /**
777
- * Carry the device link of an account **this node does not hold**, so the
778
- * namespace binds that device's key to the account.
779
- *
780
- * The relay half of a nodeless account minting invitations. An account added
781
- * to a group by account holds its grants but has no device bound, so peers
782
- * refuse whatever its device signs as "bound to no account". The credential
783
- * and scope are root-signed and self-certifying; this node adds its own
784
- * member endorsement and publishes the `AccountDeviceLinked`. After that the
785
- * device signs invitations itself — see `signGroupInvitation`.
786
- *
787
- * Refusals: `400` when a proof does not verify or the scope does not reach
788
- * this namespace (re-sign it), `403` when the device was revoked here or the
789
- * account is a member of nothing in the namespace (add it first).
790
- */
791
- async linkAccountDevice(namespaceId, request) {
792
- return unwrap(await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/account/link-device`, request));
793
- }
794
- // ---- Group Management ----
795
- async getGroupInfo(groupId) {
796
- return unwrap(await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}`));
797
- }
798
- /** Thin wrapper over {@link getGroupInfo}: returns the group's `defaultCapabilities` bitmask. */
799
- async getDefaultCapabilities(groupId) {
800
- return (await this.getGroupInfo(groupId)).defaultCapabilities;
801
- }
802
- /** Thin wrapper over {@link getGroupInfo}: returns the group's `subgroupVisibility`. */
803
- async getSubgroupVisibility(groupId) {
804
- return (await this.getGroupInfo(groupId)).subgroupVisibility;
805
- }
806
- async deleteGroup(groupId) {
807
- // Core requires `Content-Type: application/json` on this DELETE even though
808
- // the body is always empty, so send the header and an empty JSON object.
809
- return unwrap(await this.httpClient.request(`/admin-api/groups/${pathSegment(groupId)}`, {
810
- method: 'DELETE',
811
- body: '{}',
812
- headers: { 'Content-Type': 'application/json' },
813
- }));
814
- }
815
- async listGroupMembers(groupId) {
816
- const response = await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/members`);
817
- // Validate the field we declare as non-optional in the type so a
818
- // contract-violating response (proxy error body, future API drift,
819
- // etc.) surfaces as a clear error rather than silently producing an
820
- // empty list. Empty groups still satisfy this — merod returns
821
- // `members: []`, not an omitted field.
822
- if (!Array.isArray(response?.members)) {
823
- // Sanitize before interpolation: groupId reaches us from caller code,
824
- // not parsed input, but defending the message keeps untrusted bytes
825
- // out of error logs and downstream UIs.
826
- const safeId = String(groupId).replace(/[\r\n\t\s]/g, '').slice(0, 64);
827
- throw new Error(`Invalid listGroupMembers response for group ${safeId}: missing or non-array \`members\` field`);
828
- }
829
- return response;
830
- }
831
- /**
832
- * Account -> devices for the members of a group, as its live bindings record
833
- * them.
834
- *
835
- * The join between a group member listing, which names accounts, and a
836
- * context's identities, which name bare signing keys - both 64 hex, and
837
- * nothing but the source tells them apart. Batch rather than per-account
838
- * because the key -> account direction gets asked about arbitrary authors,
839
- * which a per-account route would answer in N calls.
840
- *
841
- * What comes back is **caller-scoped by the node**: an admin of the group sees
842
- * every member's devices, a plain member only its own entry. A single-entry
843
- * answer is therefore a statement about this caller's role, not about the
844
- * group's size.
845
- *
846
- * Only live bindings appear, so a revoked device is absent here - use
847
- * {@link listAccountDevices} for the account's own devices, which keeps a
848
- * revoked one with `revoked: true`. Absence here never means the device never
849
- * acted: revocation is forward-only and its past ops remain in the DAG.
850
- *
851
- * Wrapped in `members` on the wire rather than `data`, unwrapped here for the
852
- * same reason {@link listAccountDevices} unwraps `devices`.
853
- */
854
- async listGroupMemberDevices(groupId, options) {
855
- // Both params are optional on the node, which clamps `limit` to its own
856
- // maximum; sending them only when asked keeps the node's defaults authoritative
857
- // rather than freezing today's values into this client.
858
- const params = new URLSearchParams();
859
- if (options?.offset !== undefined)
860
- params.set('offset', String(options.offset));
861
- if (options?.limit !== undefined)
862
- params.set('limit', String(options.limit));
863
- const query = params.toString();
864
- const base = `/admin-api/groups/${pathSegment(groupId)}/member-devices`;
865
- const response = await this.httpClient.get(query ? `${base}?${query}` : base);
866
- return response.members;
867
- }
868
- /**
869
- * Member accounts in the group and the devices bound to each. An admin node
870
- * sees every account; a plain member only its own. `403` if this node is not a
871
- * member of the group.
872
- */
873
- async listMemberDevices(groupId, options = {}) {
874
- const params = new URLSearchParams();
875
- if (options.offset !== undefined)
876
- params.set('offset', String(options.offset));
877
- if (options.limit !== undefined)
878
- params.set('limit', String(options.limit));
879
- const query = params.toString();
880
- const path = `/admin-api/groups/${pathSegment(groupId)}/member-devices${query ? `?${query}` : ''}`;
881
- return this.httpClient.get(path);
882
- }
883
- /**
884
- * Seal a small payload to a member account's root key. This node must be a
885
- * member of the group, and `account` must be an account the group knows, so
886
- * the route reaches no further than the caller already does.
887
- */
888
- async sealToAccount(groupId, account, request) {
889
- return unwrap(await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/accounts/${pathSegment(account)}/seal`, request));
890
- }
891
- async listGroupContexts(groupId) {
892
- return unwrap(await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/contexts`));
893
- }
894
- async addGroupMembers(groupId, request) {
895
- await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/members`, request);
896
- }
897
- async removeGroupMembers(groupId, request) {
898
- await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/members/remove`, request);
899
- }
900
- /**
901
- * `identity` is the member's ACCOUNT (64 hex), as returned by
902
- * {@link listGroupMembers} — not the signing key that added them. Both are
903
- * 64 hex, so nothing but the source tells them apart.
904
- */
905
- async updateMemberRole(groupId, identity, request) {
906
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/members/${pathSegment(identity)}/role`, request);
907
- }
908
- /**
909
- * `identity` is the member's ACCOUNT (64 hex), as returned by
910
- * {@link listGroupMembers} — not the signing key that added them. Both are
911
- * 64 hex, so nothing but the source tells them apart.
912
- */
913
- async getMemberCapabilities(groupId, identity) {
914
- return unwrap(await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/members/${pathSegment(identity)}/capabilities`));
915
- }
916
- /**
917
- * `identity` is the member's ACCOUNT (64 hex), as returned by
918
- * {@link listGroupMembers} — not the signing key that added them. Both are
919
- * 64 hex, so nothing but the source tells them apart.
920
- */
921
- async setMemberCapabilities(groupId, identity, request) {
922
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/members/${pathSegment(identity)}/capabilities`, request);
923
- }
924
- async setDefaultCapabilities(groupId, request) {
925
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/settings/default-capabilities`, request);
926
- }
927
- /**
928
- * Make every member admitted to `groupId` from now on able to relay a
929
- * delegated write, without a per-node governance op each time.
930
- *
931
- * `CAN_AUTHOR_ON_BEHALF` is implied by nothing — not membership, not admin —
932
- * so every group is authorship-closed and each fleet relay otherwise needs an
933
- * admin to grant it individually, at the moment the relay is assigned and the
934
- * admin is not watching. Setting it in the group's DEFAULT mask is core's own
935
- * answer to that: `add_member` seeds a non-admin's capability row from the
936
- * default, so an attested node admitted afterwards lands open.
937
- *
938
- * Call it on the NAMESPACE (the root group). A grant resolves through the
939
- * membership anchor, so one at the root reaches every Open subgroup beneath
940
- * it, which is where contexts actually live; a `Restricted` subgroup is a
941
- * membership boundary and needs its own call.
942
- *
943
- * ### Two things this does not do
944
- *
945
- * **It is not retroactive.** The default is copied into a member's row when
946
- * that member is admitted, so nodes already admitted keep the row they got.
947
- * Use {@link grantAuthorship} for those — or call this at namespace creation,
948
- * which is the only point that needs no backfill at all.
949
- *
950
- * **It does not reach admins.** Core seeds the default only for non-admin
951
- * roles, and `CAN_AUTHOR_ON_BEHALF` is deliberately not implied by admin. An
952
- * admin's own node therefore stays closed and needs {@link grantAuthorship}.
953
- *
954
- * ### What it widens
955
- *
956
- * Every future non-admin member of the group, not only attested TEE nodes, so
957
- * any of them may be named as a warrant's `executor`. The bound worth knowing
958
- * before deciding: an executor cannot FORGE. A delegated write is authorized
959
- * by the author's own warrant — signed by their device key, committed to one
960
- * context, method and exact arguments, with its nonce checked unspent — so a
961
- * holder can only spend a warrant it was deliberately handed. What it does
962
- * gain is the arguments in cleartext and the choice of when, or whether, to
963
- * publish.
964
- *
965
- * Read-modify-write rather than a blind set: core seeds a new group's default
966
- * with `CAN_JOIN_OPEN_SUBGROUPS`, and overwriting that would quietly stop
967
- * every later member inheriting into Open subgroups.
968
- *
969
- * Returns the resulting mask and whether an op was published — it is a no-op
970
- * when the bit is already set, so it is safe to call on every startup.
971
- */
972
- async openToDelegatedExecution(groupId) {
973
- const current = await this.getDefaultCapabilities(groupId);
974
- if (hasCap(current, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
975
- return { changed: false, defaultCapabilities: current };
976
- }
977
- const next = withCap(current, CAPABILITIES.CAN_AUTHOR_ON_BEHALF);
978
- await this.setDefaultCapabilities(groupId, { defaultCapabilities: next });
979
- return { changed: true, defaultCapabilities: next };
980
- }
981
- /**
982
- * Grant `account` the authorship bit on `groupId`, leaving its other
983
- * capabilities alone.
984
- *
985
- * The per-member counterpart to {@link openToDelegatedExecution}, and what
986
- * closes the gap that method cannot: a relay admitted BEFORE the default was
987
- * set, and an admin's own node, which never receives the default at all.
988
- *
989
- * `account` is the member's ACCOUNT id (64 hex) as `listGroupMembers` returns
990
- * it — for a fleet relay, the `account` the cloud reports for that node. Not
991
- * the signing key that added them; both are 64 hex, so nothing but the source
992
- * tells them apart.
993
- *
994
- * Read-modify-write, so it adds the bit rather than replacing the member's
995
- * mask. A no-op when the member already holds it.
996
- */
997
- async grantAuthorship(groupId, account) {
998
- const { capabilities } = await this.getMemberCapabilities(groupId, account);
999
- if (hasCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF)) {
1000
- return { changed: false, capabilities };
1001
- }
1002
- const next = withCap(capabilities, CAPABILITIES.CAN_AUTHOR_ON_BEHALF);
1003
- await this.setMemberCapabilities(groupId, account, { capabilities: next });
1004
- return { changed: true, capabilities: next };
1005
- }
1006
- async setSubgroupVisibility(groupId, request) {
1007
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/settings/subgroup-visibility`, request);
1008
- }
1009
- async setTeeAdmissionPolicy(groupId, request) {
1010
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/settings/tee-admission-policy`, request);
1011
- }
1012
- async getTeeAdmissionPolicy(groupId) {
1013
- // Intersection (not union) so the type is unambiguous: `data` is optional, so
1014
- // `?? response` cleanly falls back to a flat (un-enveloped) response.
1015
- const response = await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/settings/tee-admission-policy`);
1016
- return response.data ?? response;
1017
- }
1018
- /**
1019
- * Sets which admitted TEEs may author as the TEE authority. Pass
1020
- * `{ allowedMrtd: [] }` to turn TEE authorship off. `groupId` must be a
1021
- * namespace root; the node refuses a subgroup. The node exposes no read-back
1022
- * route for this policy.
1023
- */
1024
- async setTeeAuthoringPolicy(groupId, request) {
1025
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/settings/tee-authoring-policy`, request);
1026
- }
1027
- /**
1028
- * Turns TEE authorship off in the namespace: every TEE loses its authority,
1029
- * and admitted TEEs stay members. The node applies the same op as
1030
- * `setTeeAuthoringPolicy(groupId, { allowedMrtd: [] })`. `groupId` must be a
1031
- * namespace root.
1032
- *
1033
- * `rootProof` is the signing admin's own root proof for that empty policy
1034
- * (`teeAuthoringPolicyOp([])`); omit it on a node that holds that root.
1035
- */
1036
- async disableTeeAuthoringPolicy(groupId, options = {}) {
1037
- const path = `/admin-api/groups/${pathSegment(groupId)}/settings/tee-authoring-policy`;
1038
- if (options.rootProof === undefined) {
1039
- await this.httpClient.delete(path);
1040
- return;
1041
- }
1042
- await this.httpClient.request(path, {
1043
- method: 'DELETE',
1044
- body: JSON.stringify({ rootProof: options.rootProof }),
1045
- headers: { 'Content-Type': 'application/json' },
1046
- });
1047
- }
1048
- // ---- Root-guarded owner ops (core schema 18) ----
1049
- /**
1050
- * Hand the group to `newOwner`, who must already be one of its admins.
1051
- * Owner-only, and it needs the owner account's root proof; see
1052
- * {@link RootGuardedOpRequest}.
1053
- */
1054
- async transferOwnership(groupId, request) {
1055
- await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/transfer-ownership`, request);
1056
- }
1057
- /**
1058
- * Repoint the namespace's admin pin at `newAdmin`, a member of its root.
1059
- * Owner-only, with the owner account's root proof; see {@link RootGuardedOpRequest}.
1060
- */
1061
- async changeNamespaceAdmin(namespaceId, request) {
1062
- await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/admin`, request);
1063
- }
1064
- /**
1065
- * Delete a group through the owner-only path: it must hold no contexts, and
1066
- * the op needs the owner account's root proof. {@link deleteGroup} is the
1067
- * admin-level cascading delete.
1068
- */
1069
- async ownerDeleteGroup(groupId, request = {}) {
1070
- await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/owner-delete`, request);
1071
- }
1072
- // ---- Group / member / context metadata ----
1073
- async setGroupMetadata(groupId, request) {
1074
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/metadata`, request);
1075
- }
1076
- async getGroupMetadata(groupId) {
1077
- // Core single-envelopes the record: `{ data: MetadataRecord | null }`.
1078
- // "No record yet" is `{ data: null }` (or a bare null body on older nodes),
1079
- // so optional-chain to a clean null. Returns the full record (name + data +
1080
- // updatedAt/updatedBy), not just the data map.
1081
- const response = await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/metadata`);
1082
- return response?.data ?? null;
1083
- }
1084
- async setMemberMetadata(groupId, identity, request) {
1085
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/members/${pathSegment(identity)}/metadata`, request);
1086
- }
1087
- /**
1088
- * `identity` is the member's ACCOUNT (64 hex), as returned by
1089
- * {@link listGroupMembers} — not the signing key that added them. Both are
1090
- * 64 hex, so nothing but the source tells them apart.
1091
- */
1092
- async getMemberMetadata(groupId, identity) {
1093
- // Single-enveloped record; see getGroupMetadata.
1094
- const response = await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/members/${pathSegment(identity)}/metadata`);
1095
- return response?.data ?? null;
1096
- }
1097
- async setContextMetadata(groupId, contextId, request) {
1098
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/contexts/${pathSegment(contextId)}/metadata`, request);
1099
- }
1100
- async getContextMetadata(groupId, contextId) {
1101
- // Single-enveloped record; see getGroupMetadata.
1102
- const response = await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/contexts/${pathSegment(contextId)}/metadata`);
1103
- return response?.data ?? null;
1104
- }
1105
- async syncGroup(groupId) {
1106
- return unwrap(await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/sync`, {}));
1107
- }
1108
- async upgradeGroup(groupId, request) {
1109
- return unwrap(await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/upgrade`, request));
1110
- }
1111
- async getGroupUpgradeStatus(groupId) {
1112
- return unwrap(await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/upgrade/status`));
1113
- }
1114
- /** Releases to install before activating the upgrade, in list order (oldest first). An older server returns []. */
1115
- async getGroupMissingReleases(groupId) {
1116
- const res = await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/upgrade/status`);
1117
- return res.missingReleases ?? [];
1118
- }
1119
- /**
1120
- * The operator-facing "have all peers migrated?" rollup for a namespace.
1121
- * The handler serializes the payload directly, so there is no `{ data }`
1122
- * envelope to unwrap here (unlike most admin reads).
1123
- */
1124
- async getMigrationStatus(namespaceId) {
1125
- return this.httpClient.get(`/admin-api/groups/${pathSegment(namespaceId)}/migration-status`);
1126
- }
1127
- /** Revoked devices and withdrawn slots of a namespace; 403 unless this node is a bound member. */
1128
- async listNamespaceRevokedDevices(namespaceId) {
1129
- return this.httpClient.get(`/admin-api/namespaces/${pathSegment(namespaceId)}/revoked-devices`);
1130
- }
1131
- /** Per-group cascade-migration snapshots for a namespace. */
1132
- async getCascadeStatus(namespaceId) {
1133
- return unwrap(await this.httpClient.get(`/admin-api/groups/${pathSegment(namespaceId)}/cascade-status`));
1134
- }
1135
- /** Switch this node's contexts in a namespace onto its stored upgrade target; 409 if an `expected*` guard differs. */
1136
- async activateGroupUpgrade(namespaceId, request = {}) {
1137
- return unwrap(await this.httpClient.post(`/admin-api/groups/${pathSegment(namespaceId)}/upgrade/activate`, request));
1138
- }
1139
- async retryGroupUpgrade(groupId) {
1140
- return unwrap(await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/upgrade/retry`, {}));
1141
- }
1142
- /** Move `childGroupId` under `request.newParentId`. */
1143
- async reparentGroup(childGroupId, request) {
1144
- // Core returns this flat ({ reparented }); tolerate the { data } envelope too.
1145
- const r = await this.httpClient.post(`/admin-api/groups/${pathSegment(childGroupId)}/reparent`, request);
1146
- return (r?.data ?? r);
1147
- }
1148
- async listSubgroups(groupId) {
1149
- const response = await this.httpClient.get(`/admin-api/groups/${pathSegment(groupId)}/subgroups`);
1150
- return response.subgroups ?? response.data ?? [];
1151
- }
1152
- async detachContextFromGroup(groupId, contextId) {
1153
- await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/contexts/${pathSegment(contextId)}/remove`, {});
1154
- }
1155
- // ---- Group Invitation & Join ----
1156
- async createGroupInvitation(groupId, request) {
1157
- return unwrap(await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/invite`, request ?? {}));
1158
- }
1159
- async joinGroup(request) {
1160
- return unwrap(await this.httpClient.post('/admin-api/groups/join', request));
1161
- }
1162
- async joinSubgroupInheritance(groupId) {
1163
- return unwrap(await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/join-via-inheritance`, {}));
1164
- }
1165
- // ---- TEE ----
1166
- async getTeeInfo() {
1167
- return unwrap(await this.httpClient.get('/admin-api/tee/info'));
1168
- }
1169
- async teeAttest(request) {
1170
- return unwrap(await this.httpClient.post('/admin-api/tee/attest', request));
1171
- }
1172
- // ---- Network ----
1173
- async getPeersCount() {
1174
- return this.httpClient.get('/admin-api/peers');
1175
- }
1176
- /** Node network status (GET /admin-api/network/status). */
1177
- async getNetworkStatus() {
1178
- return this.httpClient.get('/admin-api/network/status');
1179
- }
1180
- /** Node storage/usage stats (GET /admin-api/usage). */
1181
- async getUsage() {
1182
- return this.httpClient.get('/admin-api/usage');
1183
- }
1184
- // ---- Group / context / namespace membership ----
1185
- /** Create a standalone group (POST /admin-api/groups). */
1186
- async createGroup(request) {
1187
- return unwrap(await this.httpClient.post('/admin-api/groups', request));
1188
- }
1189
- /** Leave a group (POST /admin-api/groups/:group_id/leave). */
1190
- async leaveGroup(groupId) {
1191
- await this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/leave`, {});
1192
- }
1193
- /** Leave a context (POST /admin-api/contexts/:context_id/leave). */
1194
- async leaveContext(contextId) {
1195
- await this.httpClient.post(`/admin-api/contexts/${pathSegment(contextId)}/leave`, {});
1196
- }
1197
- /**
1198
- * Leave a namespace (POST /admin-api/namespaces/:namespace_id/leave).
1199
- *
1200
- * With `purge`, every device of the account also takes the namespace off its
1201
- * disk. Core reads `purge` from the query string, so it is not in the body.
1202
- */
1203
- async leaveNamespace(namespaceId, options = {}) {
1204
- const query = options.purge ? '?purge=true' : '';
1205
- await this.httpClient.post(`/admin-api/namespaces/${pathSegment(namespaceId)}/leave${query}`, {});
1206
- }
1207
- /** Issue a group ownership proof (POST /admin-api/groups/:group_id/issue-ownership-proof). */
1208
- async issueOwnershipProof(groupId, request) {
1209
- return this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/issue-ownership-proof`, request);
1210
- }
1211
- /** Issue a namespace ownership proof (POST /admin-api/groups/:group_id/issue-namespace-ownership-proof). */
1212
- async issueNamespaceOwnershipProof(groupId, request) {
1213
- return this.httpClient.post(`/admin-api/groups/${pathSegment(groupId)}/issue-namespace-ownership-proof`, request);
1214
- }
1215
- /**
1216
- * Set a member's auto-follow flag (PUT
1217
- * /admin-api/groups/:group_id/members/:identity/auto-follow).
1218
- *
1219
- * `identity` is the member's ACCOUNT (64 hex), as returned by
1220
- * {@link listGroupMembers} — not the signing key that added them. Both are
1221
- * 64 hex, so nothing but the source tells them apart.
1222
- */
1223
- async setMemberAutoFollow(groupId, identity, request) {
1224
- await this.httpClient.put(`/admin-api/groups/${pathSegment(groupId)}/members/${pathSegment(identity)}/auto-follow`, request);
1225
- }
1226
- /** Abort a namespace migration (POST /admin-api/groups/:namespace_id/migration/abort). */
1227
- async abortMigration(namespaceId) {
1228
- return this.httpClient.post(`/admin-api/groups/${pathSegment(namespaceId)}/migration/abort`, {});
1229
- }
1230
- }