@calimero-network/mero-js 27.5.0 → 28.1.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/dist/index.cjs CHANGED
@@ -78,15 +78,18 @@ __export(index_exports, {
78
78
  createQuoteVerifier: () => createQuoteVerifier,
79
79
  createSealedFetch: () => createSealedFetch,
80
80
  createSignedReleaseVerifier: () => createSignedReleaseVerifier,
81
+ deployerKey: () => deployerKey,
81
82
  errorFrom: () => errorFrom,
82
83
  fetchAttestedKey: () => fetchAttestedKey,
83
84
  fetchGithubNodeRelease: () => fetchGithubNodeRelease,
84
85
  fetchNodeRelease: () => fetchNodeRelease,
85
86
  freshCollateral: () => freshCollateral,
86
87
  fromB64url: () => fromB64url,
88
+ frontendManifest: () => frontendManifest,
87
89
  generateOwnerKey: () => generateOwnerKey,
88
90
  orgCapabilities: () => orgCapabilities,
89
91
  ownerPublicKey: () => ownerPublicKey,
92
+ pageDeployment: () => pageDeployment,
90
93
  pageQuery: () => pageQuery,
91
94
  pendingApprovalsFrom: () => pendingApprovalsFrom,
92
95
  provisionAgent: () => provisionAgent,
@@ -1759,6 +1762,26 @@ function upgradeConsent(app, toVersion, returnTo) {
1759
1762
  }
1760
1763
 
1761
1764
  // src/cloud-v2/client.ts
1765
+ function deployerKey(deployer) {
1766
+ return "account" in deployer ? `account:${deployer.account}` : `org:${deployer.org}`;
1767
+ }
1768
+ async function frontendManifest(origin, fetcher = fetch) {
1769
+ const base = origin ?? globalThis.location?.origin;
1770
+ if (!base) throw new Error("no page origin to read the frontend manifest from; pass `origin`");
1771
+ const response = await fetcher(new URL("/.well-known/calimero.json", base).toString());
1772
+ if (!response.ok) throw await refusal(response);
1773
+ const manifest = await response.json();
1774
+ return { ...manifest, deployments: manifest.deployments ?? [] };
1775
+ }
1776
+ function pageDeployment(href) {
1777
+ const at = href ?? globalThis.location?.href;
1778
+ if (!at) return null;
1779
+ try {
1780
+ return new URL(at).searchParams.get("deployment") || null;
1781
+ } catch {
1782
+ return null;
1783
+ }
1784
+ }
1762
1785
  function seg(value) {
1763
1786
  return encodeURIComponent(value);
1764
1787
  }
@@ -2157,19 +2180,25 @@ var CalimeroClient = class _CalimeroClient {
2157
2180
  me() {
2158
2181
  return this.get("auth/me");
2159
2182
  }
2160
- /** Add another passkey (another device) to the signed-in account; answers its credential id. */
2161
- async addPasskey(passkey) {
2183
+ /**
2184
+ * Add another passkey (another device, `passkey`) to the signed-in account;
2185
+ * answers its credential id. The node asks for the approval of a passkey
2186
+ * the account already has (`412 CONFIRM_REQUIRED`, never the new one),
2187
+ * which `approver` gives.
2188
+ */
2189
+ async addPasskey(passkey, approver) {
2162
2190
  const begin = await this.post("auth/passkeys/add/begin");
2163
2191
  const registration = await passkey.create(begin.options);
2164
- const { credentialId } = await this.post("auth/passkeys/add/finish", {
2165
- challengeId: begin.challengeId,
2166
- registration
2167
- });
2192
+ const { credentialId } = await this.postConfirmed(
2193
+ "auth/passkeys/add/finish",
2194
+ { challengeId: begin.challengeId, registration },
2195
+ approver
2196
+ );
2168
2197
  return credentialId;
2169
2198
  }
2170
- /** Remove one of the account's passkeys (not the last). */
2171
- async removePasskey(credentialId) {
2172
- await this.delete(`auth/passkeys/${seg(credentialId)}`);
2199
+ /** Remove one of the account's passkeys (not the last), approved with `passkey` (one it has). */
2200
+ async removePasskey(credentialId, passkey) {
2201
+ await this.deleteConfirmed(`auth/passkeys/${seg(credentialId)}`, passkey);
2173
2202
  }
2174
2203
  /** Name one of the account's passkeys (a device label); `''` clears the name. */
2175
2204
  async labelPasskey(credentialId, label) {
@@ -2197,9 +2226,13 @@ var CalimeroClient = class _CalimeroClient {
2197
2226
  async revokeApiKey(keyId) {
2198
2227
  await this.delete(`auth/apikeys/${seg(keyId)}`);
2199
2228
  }
2200
- /** Create an API key for an agent or server. The key is returned once. */
2201
- createApiKey(options) {
2202
- return this.post("auth/apikeys", options);
2229
+ /**
2230
+ * Create an API key for an agent or server, approved with the account's
2231
+ * `passkey` (`412 CONFIRM_REQUIRED`); needs a passkey session (an API key
2232
+ * is refused `403`). The key is returned once.
2233
+ */
2234
+ createApiKey(options, passkey) {
2235
+ return this.postConfirmed("auth/apikeys", { ...options }, passkey);
2203
2236
  }
2204
2237
  // ---------------------------------------------------------------- apps
2205
2238
  /** Upload a raw WASM bundle (development clusters only); answers its hash. */
@@ -2207,14 +2240,41 @@ var CalimeroClient = class _CalimeroClient {
2207
2240
  const { hash } = await this.json("POST", "bundles", void 0, wasm);
2208
2241
  return hash;
2209
2242
  }
2210
- /** Upload a blob for `app`: `blobId` is base58 (what apps print), `id` hex. */
2211
- uploadBlob(app, data) {
2212
- return this.json("POST", `apps/${seg(app)}/blobs`, void 0, data);
2243
+ /**
2244
+ * Upload a blob for `app`: `blobId` is base58 (what apps print), `id` hex.
2245
+ * `contentType` (default `application/octet-stream`; printable ASCII, the
2246
+ * node refuses another with `400`) is sent as the upload's `Content-Type`:
2247
+ * a component app records it and serves downloads with it. A component
2248
+ * app holding as many blobs as it may answers `429 QUOTA_EXCEEDED`.
2249
+ */
2250
+ uploadBlob(app, data, contentType = {}) {
2251
+ const mime = (typeof contentType === "string" ? contentType : contentType.contentType) ?? "application/octet-stream";
2252
+ return this.json("POST", `apps/${seg(app)}/blobs`, void 0, data, mime);
2213
2253
  }
2214
2254
  /** One of `app`'s blobs, by id (base58 or hex). */
2215
2255
  async downloadBlob(app, id) {
2216
- const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}`);
2217
- return new Uint8Array(await response.arrayBuffer());
2256
+ return (await this.blobContent(app, id)).data;
2257
+ }
2258
+ /**
2259
+ * Download a component app's blob with a grant one of its runs handed out
2260
+ * (`BlobRef::grant`, up to 24 h). No session needed: the grant is the
2261
+ * permission. A bad or expired grant answers `NotFound`, as a missing blob.
2262
+ */
2263
+ async downloadBlobGranted(app, id, grant) {
2264
+ return (await this.blobContent(app, id, { grant })).data;
2265
+ }
2266
+ /**
2267
+ * {@link downloadBlob} (or, with `grant`, {@link downloadBlobGranted}) with
2268
+ * the content type the node serves it as: the uploader's, for a component
2269
+ * app; `application/octet-stream` otherwise.
2270
+ */
2271
+ async blobContent(app, id, options = {}) {
2272
+ const query = options.grant === void 0 ? "" : `?grant=${encodeURIComponent(options.grant)}`;
2273
+ const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}${query}`);
2274
+ return {
2275
+ data: new Uint8Array(await response.arrayBuffer()),
2276
+ contentType: response.headers.get("content-type") ?? "application/octet-stream"
2277
+ };
2218
2278
  }
2219
2279
  /** `app`'s blobs. */
2220
2280
  async listBlobs(app) {
@@ -2276,6 +2336,15 @@ var CalimeroClient = class _CalimeroClient {
2276
2336
  async setAppMetadata(app, metadata) {
2277
2337
  await this.post(`apps/${seg(app)}/metadata`, { metadata });
2278
2338
  }
2339
+ /**
2340
+ * A component app's xcalls: how many messages wait in its outbox, and the
2341
+ * dead letters (never delivered), newest last. Its owner's or an org
2342
+ * admin's (`403` to anyone else); `409 NOT_SUPPORTED` for an app on the
2343
+ * wasmer runtime, whose xcalls are follow-ups.
2344
+ */
2345
+ appXcalls(app) {
2346
+ return this.get(`apps/${seg(app)}/xcalls`);
2347
+ }
2279
2348
  /** What the app's storage holds: `{ keys, bytes, blobs, blobBytes }`. */
2280
2349
  appStorage(app) {
2281
2350
  return this.get(`apps/${seg(app)}/storage`);
@@ -2307,12 +2376,19 @@ var CalimeroClient = class _CalimeroClient {
2307
2376
  }
2308
2377
  /**
2309
2378
  * POST `body` to `path`, answering a `412 CONFIRM_REQUIRED` with `passkey`'s
2310
- * assertion over the node's challenge (deleting an org, approving a release, ...).
2379
+ * assertion over the node's challenge (deleting an org, approving a release,
2380
+ * adding a member, ...): the same body again, with `confirm`. Without a
2381
+ * passkey (a caller in plain JavaScript passing none) the `412` reaches the
2382
+ * caller as {@link ConfirmRequired}, carrying the challenge.
2311
2383
  */
2312
2384
  postConfirmed(path, body, passkey) {
2313
2385
  return this.confirmed("POST", path, body, passkey);
2314
2386
  }
2315
- /** {@link postConfirmed} for a DELETE (deleting an organization). */
2387
+ /**
2388
+ * {@link postConfirmed} for a DELETE (deleting an organization, removing a
2389
+ * member or a passkey): `body` (`{}` by default) goes as JSON, and the
2390
+ * confirmed retry carries `confirm` beside it.
2391
+ */
2316
2392
  deleteConfirmed(path, passkey, body = {}) {
2317
2393
  return this.confirmed("DELETE", path, body, passkey);
2318
2394
  }
@@ -2320,7 +2396,7 @@ var CalimeroClient = class _CalimeroClient {
2320
2396
  try {
2321
2397
  return await send(body);
2322
2398
  } catch (error) {
2323
- if (!(error instanceof ConfirmRequired)) throw error;
2399
+ if (!(error instanceof ConfirmRequired) || !passkey) throw error;
2324
2400
  const d = error.details;
2325
2401
  let assertion;
2326
2402
  try {
@@ -2336,7 +2412,13 @@ var CalimeroClient = class _CalimeroClient {
2336
2412
  /**
2337
2413
  * Create an app instance from a bundle hash, or from a package release
2338
2414
  * (`{ package, version?, service? }`, installed from the registry first if
2339
- * the cluster lacks it).
2415
+ * the cluster lacks it). With `deployment`, the instance runs the release
2416
+ * that deployment offers for `package` (`version`, if given, must be it)
2417
+ * and records the deployment; no install, no deployer grant. A package or
2418
+ * version it does not offer answers `400 NOT_OFFERED`, several offered
2419
+ * releases with no `version` `400 VERSION_REQUIRED`, and an unknown
2420
+ * `service` `400 NO_SUCH_SERVICE`. A page opened through `/d/{deployment}/`
2421
+ * finds its deployment with {@link pageDeployment}.
2340
2422
  */
2341
2423
  async createApp(name, bundle, options = {}) {
2342
2424
  const target = typeof bundle === "string" ? { bundle } : bundle;
@@ -2644,20 +2726,24 @@ var CalimeroClient = class _CalimeroClient {
2644
2726
  async liftReleaseDeadline(version) {
2645
2727
  await this.delete(`ops/releases/${seg(version)}/deadline`);
2646
2728
  }
2647
- /** Add a member, or change their role. */
2648
- async setMember(org, account, role) {
2649
- await this.post(`orgs/${seg(org)}/members`, { account, role });
2729
+ /** Add a member, or change their role, approved with the caller's `passkey`. */
2730
+ async setMember(org, account, role, passkey) {
2731
+ await this.postConfirmed(`orgs/${seg(org)}/members`, { account, role }, passkey);
2650
2732
  }
2651
- removeMember(org, account) {
2652
- return this.delete(`orgs/${seg(org)}/members/${seg(account)}`);
2733
+ /**
2734
+ * Remove a member, or leave (`account` the caller's own), approved with the
2735
+ * caller's `passkey`.
2736
+ */
2737
+ removeMember(org, account, passkey) {
2738
+ return this.deleteConfirmed(`orgs/${seg(org)}/members/${seg(account)}`, passkey);
2653
2739
  }
2654
2740
  /**
2655
2741
  * Set capability bits ({@link ORG_CAPABILITIES}): for a member (`account`),
2656
2742
  * for a member of one team (`account` and `team`), or the organization's
2657
- * default (neither).
2743
+ * default (neither). Approved with the caller's `passkey`.
2658
2744
  */
2659
- async setCapabilities(org, capabilities, at = {}) {
2660
- await this.post(`orgs/${seg(org)}/capabilities`, { capabilities, ...at });
2745
+ async setCapabilities(org, capabilities, passkey, at = {}) {
2746
+ await this.postConfirmed(`orgs/${seg(org)}/capabilities`, { capabilities, ...at }, passkey);
2661
2747
  }
2662
2748
  async createTeam(org, name, join, parent) {
2663
2749
  const { teamId } = await this.post(`orgs/${seg(org)}/teams`, { name, join, parent });
@@ -2677,20 +2763,110 @@ var CalimeroClient = class _CalimeroClient {
2677
2763
  async deleteTeam(org, team) {
2678
2764
  await this.delete(`orgs/${seg(org)}/teams/${seg(team)}`);
2679
2765
  }
2680
- /** Add a member to a team, or change their row: role, lead, capability bits. */
2681
- async setTeamMember(org, team, account, row = {}) {
2682
- await this.post(`orgs/${seg(org)}/teams/${seg(team)}/members`, { account, ...row });
2766
+ /**
2767
+ * Add a member to a team, or change their row: role, lead, capability bits.
2768
+ * Approved with the caller's `passkey`.
2769
+ */
2770
+ async setTeamMember(org, team, account, passkey, row = {}) {
2771
+ await this.postConfirmed(`orgs/${seg(org)}/teams/${seg(team)}/members`, { account, ...row }, passkey);
2683
2772
  }
2684
- async removeTeamMember(org, team, account) {
2685
- await this.delete(`orgs/${seg(org)}/teams/${seg(team)}/members/${seg(account)}`);
2773
+ /** Remove a member from a team, approved with the caller's `passkey`. */
2774
+ async removeTeamMember(org, team, account, passkey) {
2775
+ await this.deleteConfirmed(`orgs/${seg(org)}/teams/${seg(team)}/members/${seg(account)}`, passkey);
2686
2776
  }
2687
- async invite(org, role, teams = [], options = {}) {
2688
- const { token } = await this.post(`orgs/${seg(org)}/invites`, { role, teams, ...options });
2777
+ /**
2778
+ * Make an invite into `org` (and `teams`), approved with the caller's
2779
+ * `passkey`; answers its token, which {@link redeemInvite} takes (no
2780
+ * approval there).
2781
+ */
2782
+ async invite(org, role, passkey, teams = [], options = {}) {
2783
+ const { token } = await this.postConfirmed(
2784
+ `orgs/${seg(org)}/invites`,
2785
+ { role, teams, ...options },
2786
+ passkey
2787
+ );
2689
2788
  return token;
2690
2789
  }
2691
2790
  redeemInvite(token) {
2692
2791
  return this.post(`invites/${seg(token)}/redeem`);
2693
2792
  }
2793
+ // ---------------------------------------------------------------- deployers
2794
+ /**
2795
+ * Let an account or organization install apps where the cluster's
2796
+ * deployments are `granted`, until `until` (RFC 3339 or a `Date`; absent,
2797
+ * the grant does not end). The cluster's owner grants it, approved with
2798
+ * `passkey`, where governance says `deployers: owner`; elsewhere the
2799
+ * operator grants with its key, and this is refused `403`.
2800
+ */
2801
+ grantDeployer(deployer, passkey, options = {}) {
2802
+ const until = options.until instanceof Date ? options.until.toISOString() : options.until;
2803
+ return this.postConfirmed("deployers", { deployer, ...until === void 0 ? {} : { until } }, passkey);
2804
+ }
2805
+ /**
2806
+ * Who may install apps on the cluster: its deployments rule, who grants
2807
+ * deployers, its owner and every grant. For the cluster's owner, or the
2808
+ * operator (its token, `useApiKey`).
2809
+ */
2810
+ deployers() {
2811
+ return this.get("deployers");
2812
+ }
2813
+ /**
2814
+ * End a deployer's grant (`deployer`, or its key `account:{id}` /
2815
+ * `org:{id}`), as the cluster's owner, approved with `passkey`. Apps it
2816
+ * installed, and the apps running them, stay.
2817
+ */
2818
+ async revokeDeployer(deployer, passkey) {
2819
+ const key = typeof deployer === "string" ? deployer : deployerKey(deployer);
2820
+ await this.deleteConfirmed(`deployers/${seg(key)}`, passkey);
2821
+ }
2822
+ // ---------------------------------------------------------------- deployments
2823
+ /**
2824
+ * Create a deployment: `owner`'s (the caller's own account, or an
2825
+ * organization where it holds `MANAGE_APPLICATION` org-wide) list of the
2826
+ * apps it offers, named `name` (1 to 64 characters, one line) for
2827
+ * customers. Approved with `passkey`; where deployments are `granted` the
2828
+ * caller needs a deployer grant. Answers the deployment's id.
2829
+ */
2830
+ async createDeployment(name, owner, passkey) {
2831
+ const { id } = await this.postConfirmed("deployments", { name, owner }, passkey);
2832
+ return id;
2833
+ }
2834
+ /** The deployments the caller manages: its account's, and its organizations' where it holds `MANAGE_APPLICATION`. */
2835
+ async deployments() {
2836
+ const { deployments } = await this.get("deployments");
2837
+ return deployments;
2838
+ }
2839
+ /** A deployment as anyone may read it: its name, its owner's kind and name, and the apps it offers. Public. */
2840
+ deployment(id) {
2841
+ return this.get(`deployments/${seg(id)}`);
2842
+ }
2843
+ /** Delete a deployment that offers nothing and has no instances, approved with `passkey`. */
2844
+ async deleteDeployment(id, passkey) {
2845
+ await this.deleteConfirmed(`deployments/${seg(id)}`, passkey);
2846
+ }
2847
+ /**
2848
+ * Offer `package@version` (installed, and seen by the caller) as `app`
2849
+ * (1 to 64 letters, digits, `.`, `_`, `-`) in deployment `id`, or move an
2850
+ * offered app to another release of its package. Approved with `passkey`.
2851
+ */
2852
+ offerApp(id, offer, passkey) {
2853
+ return this.postConfirmed(`deployments/${seg(id)}/apps`, { ...offer }, passkey);
2854
+ }
2855
+ /**
2856
+ * Withdraw `app` from deployment `id`, approved with `passkey`; refused
2857
+ * while an instance created from the deployment still runs it.
2858
+ */
2859
+ async withdrawApp(id, app, passkey) {
2860
+ await this.deleteConfirmed(`deployments/${seg(id)}/apps/${seg(app)}`, passkey);
2861
+ }
2862
+ /**
2863
+ * The organizations with instances created from deployment `id`, for its
2864
+ * managers: each one's id, name, instance count, apps and releases, and
2865
+ * first instance. Never members, teams or app data.
2866
+ */
2867
+ deploymentOrgs(id) {
2868
+ return this.get(`deployments/${seg(id)}/orgs`);
2869
+ }
2694
2870
  };
2695
2871
 
2696
2872
  // src/cloud-v2/capabilities.ts