@tinycloud/mcp 0.4.1-beta.0 → 0.5.0-beta.10

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/http.cjs CHANGED
@@ -27,6 +27,7 @@ var import_server4 = require("@modelcontextprotocol/server");
27
27
 
28
28
  // src/oauth.ts
29
29
  var import_server = require("@modelcontextprotocol/server");
30
+ var import_node_sdk = require("@tinycloud/node-sdk");
30
31
  var import_jose = require("jose");
31
32
  var OWNER_DIDS_CLAIM = "https://tinycloud.xyz/owner_dids";
32
33
  function createOpenKeyTokenVerifier(config) {
@@ -63,11 +64,11 @@ function authInfo(token, payload, config) {
63
64
  );
64
65
  }
65
66
  const clientId = firstString(payload.azp, payload.client_id) ?? "unknown";
66
- const ownerDids = Array.isArray(payload[OWNER_DIDS_CLAIM]) ? payload[OWNER_DIDS_CLAIM].filter((value) => typeof value === "string") : [];
67
- if (ownerDids.length === 0) {
67
+ const ownerDids = canonicalOwnerDids(payload[OWNER_DIDS_CLAIM]);
68
+ if (ownerDids === void 0) {
68
69
  throw new import_server.OAuthError(
69
70
  import_server.OAuthErrorCode.InvalidToken,
70
- "The OpenKey access token has no TinyCloud owner identity."
71
+ "The OpenKey access token has no valid TinyCloud owner identity."
71
72
  );
72
73
  }
73
74
  return {
@@ -82,6 +83,21 @@ function authInfo(token, payload, config) {
82
83
  }
83
84
  };
84
85
  }
86
+ var OWNER_DID = /^did:pkh:eip155:([1-9][0-9]{0,15}):(0x[0-9a-fA-F]{40})$/;
87
+ function canonicalOwnerDids(claim) {
88
+ if (!Array.isArray(claim) || claim.length === 0) return void 0;
89
+ const ownerDids = [];
90
+ for (const value of claim) {
91
+ const match = typeof value === "string" ? OWNER_DID.exec(value) : null;
92
+ if (match === null) return void 0;
93
+ try {
94
+ ownerDids.push((0, import_node_sdk.pkhDid)(match[2], Number(match[1])));
95
+ } catch {
96
+ return void 0;
97
+ }
98
+ }
99
+ return [...new Set(ownerDids)];
100
+ }
85
101
  function firstString(...values) {
86
102
  return values.find((value) => typeof value === "string" && value.length > 0);
87
103
  }
@@ -90,7 +106,7 @@ function firstString(...values) {
90
106
  var import_node_crypto = require("crypto");
91
107
  var import_promises = require("fs/promises");
92
108
  var import_node_path = require("path");
93
- var import_node_sdk = require("@tinycloud/node-sdk");
109
+ var import_node_sdk2 = require("@tinycloud/node-sdk");
94
110
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
95
111
  var import_operations = require("@tinycloud/operations");
96
112
  var PROFILE = "agent";
@@ -204,8 +220,7 @@ var RemoteTenantStore = class {
204
220
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
205
221
  throw new ApprovalOwnerMismatchError();
206
222
  }
207
- const granted = [...node.getVerifiedSessionCapabilities()];
208
- if (!permissionsCover(pending.permissions, granted)) {
223
+ if (!(0, import_node_sdk2.isCapabilitySubset)(pending.permissions, node.getVerifiedSessionCapabilities()).subset) {
209
224
  throw new Error("The approved delegation does not cover the requested account access.");
210
225
  }
211
226
  const profile = await readJson(this.#profilePath(tenant.stateRoot));
@@ -221,14 +236,25 @@ var RemoteTenantStore = class {
221
236
  }
222
237
  async #storeDelegation(tenant, pending, data) {
223
238
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
224
- const { node } = await this.#restoreApproval(tenant, data);
239
+ const { node, session } = await this.#restoreApproval(tenant, data);
225
240
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
226
- const delegation = portableFromCallback(data, pending.permissions, this.#config.nodeHost);
241
+ const delegation = (0, import_node_sdk2.siweRuntimeDelegation)(node, {
242
+ authorization: session.delegationHeader.Authorization,
243
+ cid: session.delegationCid,
244
+ spaceId: session.spaceId,
245
+ ownerAddress: session.address,
246
+ chainId: session.chainId,
247
+ siwe: session.siwe,
248
+ signature: session.signature,
249
+ host: this.#config.nodeHost
250
+ });
227
251
  const result = await (0, import_operations.invokeOperation)(
228
252
  "tinycloud.auth.import",
229
253
  1,
230
254
  { profile: PROFILE, stateRoot: tenant.stateRoot },
231
255
  {
256
+ kind: "tinycloud.auth.delegation",
257
+ version: 1,
232
258
  requestId: pending.requestId,
233
259
  delegationCid: delegation.cid,
234
260
  delegation
@@ -238,12 +264,14 @@ var RemoteTenantStore = class {
238
264
  }
239
265
  /**
240
266
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
241
- * WASM verifier binds the authorization bytes and CID to the signed SIWE, so
242
- * `node.did` is the wallet that signed exactly the delegation being stored.
267
+ * callback must carry the full signed proof (SIWE, signature, address and
268
+ * chain), so the WASM verifier always runs: it binds the authorization bytes
269
+ * and CID to the signed SIWE, and `node.did` is the wallet that signed
270
+ * exactly the delegation being stored.
243
271
  */
244
272
  async #restoreApproval(tenant, data) {
245
273
  const session = sessionFromCallback(data, tenant.jwk);
246
- const node = new import_node_sdk.TinyCloudNode({ host: this.#config.nodeHost });
274
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
247
275
  await node.restoreSession(session);
248
276
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
249
277
  throw new Error("The approved delegation targets a different session.");
@@ -264,7 +292,7 @@ var RemoteTenantStore = class {
264
292
  const raw = await readJson(this.#sessionPath(tenant.stateRoot));
265
293
  if (raw === null) return false;
266
294
  try {
267
- const node = new import_node_sdk.TinyCloudNode({ host: this.#config.nodeHost });
295
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
268
296
  await node.restoreSession(raw);
269
297
  return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
270
298
  } catch {
@@ -419,7 +447,7 @@ function ensureWasm() {
419
447
  }
420
448
  function sessionFromCallback(data, privateJwk) {
421
449
  const delegationHeader = data.delegationHeader;
422
- if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string") {
450
+ if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string" || typeof data.address !== "string" || typeof data.chainId !== "number" || typeof data.siwe !== "string" || typeof data.signature !== "string") {
423
451
  throw new Error("OpenKey returned an incomplete delegation.");
424
452
  }
425
453
  return {
@@ -428,63 +456,12 @@ function sessionFromCallback(data, privateJwk) {
428
456
  spaceId: data.spaceId,
429
457
  jwk: privateJwk,
430
458
  verificationMethod: data.verificationMethod,
431
- ...typeof data.address === "string" ? { address: data.address } : {},
432
- ...typeof data.chainId === "number" ? { chainId: data.chainId } : {},
433
- ...typeof data.siwe === "string" ? { siwe: data.siwe } : {},
434
- ...typeof data.signature === "string" ? { signature: data.signature } : {}
435
- };
436
- }
437
- function portableFromCallback(data, permissions, host) {
438
- const primary = permissions.find((permission) => permission.service !== "tinycloud.encryption") ?? permissions[0];
439
- if (primary === void 0) throw new Error("The authority request has no permissions.");
440
- const session = sessionFromCallback(data, {});
441
- const expiry = delegationExpiry(data);
442
- return {
443
- cid: session.delegationCid,
444
- delegationHeader: session.delegationHeader,
445
- spaceId: session.spaceId,
446
- path: primary.path,
447
- actions: [...primary.actions],
448
- resources: permissions.map((permission) => ({
449
- service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
450
- space: permission.service === "tinycloud.encryption" ? requiredPermissionSpace(permission) : session.spaceId,
451
- path: permission.path,
452
- actions: [...permission.actions]
453
- })),
454
- expiry,
455
- delegateDID: session.verificationMethod,
456
- ownerAddress: typeof data.address === "string" ? data.address : "",
457
- chainId: typeof data.chainId === "number" ? data.chainId : 1,
458
- host
459
+ address: data.address,
460
+ chainId: data.chainId,
461
+ siwe: data.siwe,
462
+ signature: data.signature
459
463
  };
460
464
  }
461
- function delegationExpiry(data) {
462
- for (const key of ["expiry", "expiresAt", "expirationTime"]) {
463
- const parsed = parseDate(data[key]);
464
- if (parsed !== void 0) return parsed;
465
- }
466
- if (typeof data.siwe === "string") {
467
- const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
468
- const parsed = parseDate(match?.[1]?.trim());
469
- if (parsed !== void 0) return parsed;
470
- }
471
- throw new Error("OpenKey did not return a delegation expiry.");
472
- }
473
- function permissionsCover(requested, granted) {
474
- return requested.every((request) => granted.some(
475
- (grant) => normalizeService(grant.service) === normalizeService(request.service) && typeof grant.space === "string" && typeof request.space === "string" && sameSpace(grant.space, request.space) && grant.path === request.path && request.actions.every((action) => grant.actions.includes(action))
476
- ));
477
- }
478
- function sameSpace(granted, requested) {
479
- return granted === requested || granted.endsWith(`:${requested}`);
480
- }
481
- function normalizeService(service) {
482
- return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
483
- }
484
- function requiredPermissionSpace(permission) {
485
- if (typeof permission.space !== "string") throw new Error("The authority request has no space.");
486
- return permission.space;
487
- }
488
465
  function publicJwk(jwk) {
489
466
  const copy = { ...jwk };
490
467
  for (const field of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) delete copy[field];
@@ -493,7 +470,7 @@ function publicJwk(jwk) {
493
470
  function samePrincipal(left, right) {
494
471
  if (typeof left !== "string") return false;
495
472
  try {
496
- return (0, import_node_sdk.principalDidEquals)(left, right);
473
+ return (0, import_node_sdk2.principalDidEquals)(left, right);
497
474
  } catch {
498
475
  return false;
499
476
  }
@@ -507,11 +484,6 @@ function isPermissionEntry(value) {
507
484
  function isRecord(value) {
508
485
  return typeof value === "object" && value !== null && !Array.isArray(value);
509
486
  }
510
- function parseDate(value) {
511
- if (typeof value !== "string" && typeof value !== "number") return void 0;
512
- const date = new Date(typeof value === "number" && value < 1e10 ? value * 1e3 : value);
513
- return Number.isNaN(date.getTime()) ? void 0 : date;
514
- }
515
487
  async function secureDirectory(path) {
516
488
  await (0, import_promises.mkdir)(path, { recursive: true, mode: 448 });
517
489
  await (0, import_promises.chmod)(path, 448);
@@ -581,8 +553,8 @@ var import_ajv = require("@modelcontextprotocol/server/validators/ajv");
581
553
  var import_operations2 = require("@tinycloud/operations");
582
554
 
583
555
  // src/results.ts
584
- function toMcpToolResult(result) {
585
- const canonical = isCanonicalOperationResult(result) ? result : {
556
+ function toMcpToolResult(result, bindings = []) {
557
+ const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
586
558
  status: "error",
587
559
  operation: {},
588
560
  context: {},
@@ -593,6 +565,18 @@ function toMcpToolResult(result) {
593
565
  structuredContent: canonical
594
566
  };
595
567
  }
568
+ function withRecoveryTool(result, bindings) {
569
+ if (result.status !== "error") return result;
570
+ const error = result.error;
571
+ const recovery = error?.details?.recovery;
572
+ if (recovery?.action !== "complete_migration") return result;
573
+ const binding = bindings.find((candidate) => candidate.operationId === recovery.operationId);
574
+ if (binding === void 0) return result;
575
+ return {
576
+ ...result,
577
+ error: { ...error, details: { ...error?.details, recovery: { ...recovery, tool: binding.name } } }
578
+ };
579
+ }
596
580
  function isCanonicalOperationResult(value) {
597
581
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
598
582
  const status = value.status;
@@ -626,6 +610,10 @@ var packageRequire = (0, import_node_module.createRequire)(
626
610
  var generatedCatalog = packageRequire(
627
611
  "@tinycloud/operations/operations.json"
628
612
  );
613
+ var ALL_SURFACES = ["local", "hosted"];
614
+ function bindingSurfaces(binding) {
615
+ return binding.surfaces ?? ALL_SURFACES;
616
+ }
629
617
  var TOOL_BINDINGS = [
630
618
  {
631
619
  name: "tinycloud_status",
@@ -670,7 +658,10 @@ var TOOL_BINDINGS = [
670
658
  readOnlyHint: false,
671
659
  idempotentHint: true,
672
660
  destructiveHint: false,
673
- openWorldHint: false
661
+ openWorldHint: false,
662
+ // Hosted delegations must arrive through the owner/request-bound OpenKey callback,
663
+ // not a caller-supplied import artifact.
664
+ surfaces: ["local"]
674
665
  },
675
666
  {
676
667
  name: "tinycloud_account_spaces_list",
@@ -770,6 +761,18 @@ var TOOL_BINDINGS = [
770
761
  idempotentHint: true,
771
762
  destructiveHint: false,
772
763
  openWorldHint: true
764
+ },
765
+ {
766
+ name: "tinycloud_secrets_status",
767
+ operationId: "tinycloud.secrets.status",
768
+ operationVersion: 1,
769
+ readOnlyHint: true,
770
+ idempotentHint: true,
771
+ destructiveHint: false,
772
+ openWorldHint: true,
773
+ // Hosted has no persistent reauthorization path for a revoked session
774
+ // yet (TC-896), so status is local/stdio only.
775
+ surfaces: ["local"]
773
776
  }
774
777
  ];
775
778
  function createJsonSchemaValidator() {
@@ -785,6 +788,7 @@ function registerTinyCloudTools(server, selection, validator) {
785
788
  ])
786
789
  );
787
790
  for (const binding of TOOL_BINDINGS) {
791
+ if (!bindingSurfaces(binding).includes(selection.surface)) continue;
788
792
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
789
793
  if (operation === void 0) {
790
794
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -820,7 +824,7 @@ function registerTinyCloudTools(server, selection, validator) {
820
824
  input
821
825
  );
822
826
  const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
823
- return toMcpToolResult(projected);
827
+ return toMcpToolResult(projected, TOOL_BINDINGS);
824
828
  }
825
829
  );
826
830
  }
@@ -829,7 +833,7 @@ function registerTinyCloudTools(server, selection, validator) {
829
833
  // package.json
830
834
  var package_default = {
831
835
  name: "@tinycloud/mcp",
832
- version: "0.4.1-beta.0",
836
+ version: "0.5.0-beta.10",
833
837
  description: "TinyCloud delegated operations MCP server",
834
838
  keywords: [
835
839
  "tinycloud",
@@ -900,9 +904,9 @@ var package_default = {
900
904
  dependencies: {
901
905
  "@modelcontextprotocol/node": "2.0.0-beta.4",
902
906
  "@modelcontextprotocol/server": "2.0.0-beta.4",
903
- "@tinycloud/node-sdk": "3.2.0-beta.0",
907
+ "@tinycloud/node-sdk": "3.2.0-beta.9",
904
908
  "@tinycloud/node-sdk-wasm": "1.7.7",
905
- "@tinycloud/operations": "0.5.0-beta.0",
909
+ "@tinycloud/operations": "0.5.0-beta.10",
906
910
  jose: "6.1.3"
907
911
  },
908
912
  devDependencies: {
@@ -956,6 +960,7 @@ function createHostedMcpApp(config) {
956
960
  }
957
961
  );
958
962
  registerTinyCloudTools(server, {
963
+ surface: "hosted",
959
964
  profile: "agent",
960
965
  explicitProfile: true,
961
966
  allowOwnerProfile: false,