@tinycloud/mcp 0.5.4-beta.0 → 0.5.4-beta.1

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.js CHANGED
@@ -315,7 +315,7 @@ function registerTinyCloudTools(server, selection, validator) {
315
315
  // package.json
316
316
  var package_default = {
317
317
  name: "@tinycloud/mcp",
318
- version: "0.5.4-beta.0",
318
+ version: "0.5.4-beta.1",
319
319
  description: "TinyCloud delegated operations MCP server",
320
320
  keywords: [
321
321
  "tinycloud",
@@ -388,7 +388,7 @@ var package_default = {
388
388
  "@modelcontextprotocol/server": "2.0.0-beta.4",
389
389
  "@tinycloud/node-sdk": "3.2.4-beta.0",
390
390
  "@tinycloud/node-sdk-wasm": "1.7.7",
391
- "@tinycloud/operations": "0.5.4-beta.0",
391
+ "@tinycloud/operations": "0.5.4-beta.1",
392
392
  jose: "6.1.3"
393
393
  },
394
394
  devDependencies: {
@@ -536,6 +536,7 @@ import {
536
536
  oauthMetadataResponse,
537
537
  requireBearerAuth
538
538
  } from "@modelcontextprotocol/server";
539
+ import { RUNTIME_DELEGATION_REJECTIONS } from "@tinycloud/node-sdk";
539
540
 
540
541
  // src/oauth.ts
541
542
  import {
@@ -622,7 +623,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
622
623
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
623
624
  import { dirname, join as join2 } from "path";
624
625
  import {
626
+ InvalidRestoredSessionError,
627
+ RuntimeDelegationRejectedError,
625
628
  TinyCloudNode,
629
+ UnsupportedSessionRestoreError,
626
630
  isCapabilitySubset,
627
631
  principalDidEquals,
628
632
  siweRuntimeDelegation
@@ -651,6 +655,26 @@ var ApprovalOwnerMismatchError = class extends Error {
651
655
  this.name = "ApprovalOwnerMismatchError";
652
656
  }
653
657
  };
658
+ var ApprovalStageError = class extends Error {
659
+ stage;
660
+ code;
661
+ reason;
662
+ constructor(stage, code, reason, cause) {
663
+ super(`The approval failed at ${stage} (${code}).`, { cause });
664
+ this.name = "ApprovalStageError";
665
+ this.stage = stage;
666
+ this.code = code;
667
+ this.reason = reason;
668
+ }
669
+ };
670
+ var ApprovalRequestError = class extends Error {
671
+ detail;
672
+ constructor(detail, message, cause) {
673
+ super(message, { cause });
674
+ this.name = "ApprovalRequestError";
675
+ this.detail = detail;
676
+ }
677
+ };
654
678
  var RemoteTenantStore = class {
655
679
  #config;
656
680
  #initializing = /* @__PURE__ */ new Map();
@@ -716,11 +740,20 @@ var RemoteTenantStore = class {
716
740
  }
717
741
  async completeApproval(state, request) {
718
742
  const length = Number(request.headers.get("content-length") ?? "0");
719
- if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
743
+ if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
744
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
745
+ }
720
746
  const text = await request.text();
721
- if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
722
- const data = JSON.parse(text);
723
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
747
+ if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
748
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
749
+ }
750
+ let data;
751
+ try {
752
+ data = JSON.parse(text);
753
+ } catch (error) {
754
+ throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
755
+ }
756
+ if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
724
757
  const pending = await this.#readPending(state, true);
725
758
  try {
726
759
  const tenant = await this.#readTenant(pending.tenantId);
@@ -758,16 +791,26 @@ var RemoteTenantStore = class {
758
791
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
759
792
  const { node, session } = await this.#restoreApproval(tenant, data);
760
793
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
761
- const delegation = siweRuntimeDelegation(node, {
762
- authorization: session.delegationHeader.Authorization,
763
- cid: session.delegationCid,
764
- spaceId: session.spaceId,
765
- ownerAddress: session.address,
766
- chainId: session.chainId,
767
- siwe: session.siwe,
768
- signature: session.signature,
769
- host: this.#config.nodeHost
770
- });
794
+ let delegation;
795
+ try {
796
+ delegation = siweRuntimeDelegation(node, {
797
+ authorization: session.delegationHeader.Authorization,
798
+ cid: session.delegationCid,
799
+ spaceId: session.spaceId,
800
+ ownerAddress: session.address,
801
+ chainId: session.chainId,
802
+ siwe: session.siwe,
803
+ signature: session.signature,
804
+ host: this.#config.nodeHost
805
+ });
806
+ } catch (error) {
807
+ throw new ApprovalStageError(
808
+ "siwe_runtime_delegation",
809
+ "DELEGATION_REJECTED",
810
+ error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
811
+ error
812
+ );
813
+ }
771
814
  const result = await invokeOperation2(
772
815
  "tinycloud.auth.import",
773
816
  1,
@@ -780,7 +823,15 @@ var RemoteTenantStore = class {
780
823
  delegation
781
824
  }
782
825
  );
783
- if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
826
+ if (result.status === "error") {
827
+ const reason = result.error.details?.reason;
828
+ throw new ApprovalStageError(
829
+ result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
830
+ result.error.code,
831
+ typeof reason === "string" ? reason : void 0
832
+ );
833
+ }
834
+ if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
784
835
  }
785
836
  /**
786
837
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
@@ -790,11 +841,25 @@ var RemoteTenantStore = class {
790
841
  * exactly the delegation being stored.
791
842
  */
792
843
  async #restoreApproval(tenant, data) {
793
- const session = sessionFromCallback(data, tenant.jwk);
844
+ let session;
845
+ try {
846
+ session = sessionFromCallback(data, tenant.jwk);
847
+ } catch (error) {
848
+ throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
849
+ }
794
850
  const node = new TinyCloudNode({ host: this.#config.nodeHost });
795
- await node.restoreSession(session);
851
+ try {
852
+ await node.restoreSession(session);
853
+ } catch (error) {
854
+ throw new ApprovalStageError(
855
+ "restore_approval",
856
+ error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
857
+ void 0,
858
+ error
859
+ );
860
+ }
796
861
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
797
- throw new Error("The approved delegation targets a different session.");
862
+ throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
798
863
  }
799
864
  return { node, session };
800
865
  }
@@ -853,10 +918,10 @@ var RemoteTenantStore = class {
853
918
  return { stateRoot, sessionDid, jwk };
854
919
  }
855
920
  async #readTenant(tenantId) {
856
- if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
921
+ if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
857
922
  const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
858
923
  const jwk = await readJson(this.#keyPath(stateRoot));
859
- if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
924
+ if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
860
925
  return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
861
926
  }
862
927
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
@@ -886,26 +951,40 @@ var RemoteTenantStore = class {
886
951
  async #readPending(state, consume) {
887
952
  const [tenantId, nonce, signature] = state.split(".");
888
953
  if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
889
- throw new Error("Approval state is invalid.");
954
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
890
955
  }
891
956
  const expected = this.#signState(tenantId, nonce);
892
957
  const supplied = Buffer.from(signature, "base64url");
893
958
  const expectedBytes = Buffer.from(expected, "base64url");
894
959
  if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
895
- throw new Error("Approval state is invalid.");
960
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
896
961
  }
897
962
  const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
898
963
  const path = this.#pendingPath(stateRoot, nonce);
899
964
  const claimed = `${path}.processing`;
900
- if (consume) await rename(path, claimed);
965
+ if (consume) {
966
+ try {
967
+ await rename(path, claimed);
968
+ } catch (error) {
969
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
970
+ const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
971
+ (jwk) => jwk !== null,
972
+ () => true
973
+ );
974
+ if (!tenantExists) {
975
+ throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
976
+ }
977
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
978
+ }
979
+ }
901
980
  try {
902
981
  const pending = await readJson(consume ? claimed : path);
903
982
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
904
- throw new Error("Approval state is invalid or already used.");
983
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
905
984
  }
906
985
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
907
986
  await rm(consume ? claimed : path, { force: true });
908
- throw new Error("Approval state has expired.");
987
+ throw new ApprovalRequestError("state_expired", "Approval state has expired.");
909
988
  }
910
989
  return pending;
911
990
  } catch (error) {
@@ -1142,8 +1221,23 @@ function createHostedMcpApp(config) {
1142
1221
  await store.completeApproval(requiredState(url), request);
1143
1222
  return callbackCors(json({ success: true }), config.openkeyHost);
1144
1223
  } catch (error) {
1224
+ if (error instanceof ApprovalOwnerMismatchError) {
1225
+ return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
1226
+ }
1227
+ const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
1228
+ const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
1229
+ const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
1230
+ const detail = error instanceof ApprovalRequestError ? error.detail : "none";
1231
+ writeDiagnostic(
1232
+ `connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
1233
+ );
1145
1234
  return callbackCors(
1146
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
1235
+ json({
1236
+ error: stage,
1237
+ code,
1238
+ ...reason === void 0 ? {} : { reason },
1239
+ message: "TinyCloud could not accept this delegation."
1240
+ }, 400),
1147
1241
  config.openkeyHost
1148
1242
  );
1149
1243
  }
@@ -1218,7 +1312,8 @@ function canonicalResourceUrl(url) {
1218
1312
  }
1219
1313
  function requiredState(url) {
1220
1314
  const state = url.searchParams.get("state");
1221
- if (state === null || state.length > 512) throw new Error("Missing approval state.");
1315
+ if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
1316
+ if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1222
1317
  return state;
1223
1318
  }
1224
1319
  function validateOrigin(request, allowed) {