@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/http.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  oauthMetadataResponse,
9
9
  requireBearerAuth
10
10
  } from "@modelcontextprotocol/server";
11
+ import { RUNTIME_DELEGATION_REJECTIONS } from "@tinycloud/node-sdk";
11
12
 
12
13
  // src/oauth.ts
13
14
  import {
@@ -94,7 +95,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
94
95
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
95
96
  import { dirname, join } from "path";
96
97
  import {
98
+ InvalidRestoredSessionError,
99
+ RuntimeDelegationRejectedError,
97
100
  TinyCloudNode,
101
+ UnsupportedSessionRestoreError,
98
102
  isCapabilitySubset,
99
103
  principalDidEquals,
100
104
  siweRuntimeDelegation
@@ -123,6 +127,26 @@ var ApprovalOwnerMismatchError = class extends Error {
123
127
  this.name = "ApprovalOwnerMismatchError";
124
128
  }
125
129
  };
130
+ var ApprovalStageError = class extends Error {
131
+ stage;
132
+ code;
133
+ reason;
134
+ constructor(stage, code, reason, cause) {
135
+ super(`The approval failed at ${stage} (${code}).`, { cause });
136
+ this.name = "ApprovalStageError";
137
+ this.stage = stage;
138
+ this.code = code;
139
+ this.reason = reason;
140
+ }
141
+ };
142
+ var ApprovalRequestError = class extends Error {
143
+ detail;
144
+ constructor(detail, message, cause) {
145
+ super(message, { cause });
146
+ this.name = "ApprovalRequestError";
147
+ this.detail = detail;
148
+ }
149
+ };
126
150
  var RemoteTenantStore = class {
127
151
  #config;
128
152
  #initializing = /* @__PURE__ */ new Map();
@@ -188,11 +212,20 @@ var RemoteTenantStore = class {
188
212
  }
189
213
  async completeApproval(state, request) {
190
214
  const length = Number(request.headers.get("content-length") ?? "0");
191
- if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
215
+ if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
216
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
217
+ }
192
218
  const text = await request.text();
193
- if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
194
- const data = JSON.parse(text);
195
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
219
+ if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
220
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
221
+ }
222
+ let data;
223
+ try {
224
+ data = JSON.parse(text);
225
+ } catch (error) {
226
+ throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
227
+ }
228
+ if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
196
229
  const pending = await this.#readPending(state, true);
197
230
  try {
198
231
  const tenant = await this.#readTenant(pending.tenantId);
@@ -230,16 +263,26 @@ var RemoteTenantStore = class {
230
263
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
231
264
  const { node, session } = await this.#restoreApproval(tenant, data);
232
265
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
233
- const delegation = siweRuntimeDelegation(node, {
234
- authorization: session.delegationHeader.Authorization,
235
- cid: session.delegationCid,
236
- spaceId: session.spaceId,
237
- ownerAddress: session.address,
238
- chainId: session.chainId,
239
- siwe: session.siwe,
240
- signature: session.signature,
241
- host: this.#config.nodeHost
242
- });
266
+ let delegation;
267
+ try {
268
+ delegation = siweRuntimeDelegation(node, {
269
+ authorization: session.delegationHeader.Authorization,
270
+ cid: session.delegationCid,
271
+ spaceId: session.spaceId,
272
+ ownerAddress: session.address,
273
+ chainId: session.chainId,
274
+ siwe: session.siwe,
275
+ signature: session.signature,
276
+ host: this.#config.nodeHost
277
+ });
278
+ } catch (error) {
279
+ throw new ApprovalStageError(
280
+ "siwe_runtime_delegation",
281
+ "DELEGATION_REJECTED",
282
+ error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
283
+ error
284
+ );
285
+ }
243
286
  const result = await invokeOperation(
244
287
  "tinycloud.auth.import",
245
288
  1,
@@ -252,7 +295,15 @@ var RemoteTenantStore = class {
252
295
  delegation
253
296
  }
254
297
  );
255
- if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
298
+ if (result.status === "error") {
299
+ const reason = result.error.details?.reason;
300
+ throw new ApprovalStageError(
301
+ result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
302
+ result.error.code,
303
+ typeof reason === "string" ? reason : void 0
304
+ );
305
+ }
306
+ if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
256
307
  }
257
308
  /**
258
309
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
@@ -262,11 +313,25 @@ var RemoteTenantStore = class {
262
313
  * exactly the delegation being stored.
263
314
  */
264
315
  async #restoreApproval(tenant, data) {
265
- const session = sessionFromCallback(data, tenant.jwk);
316
+ let session;
317
+ try {
318
+ session = sessionFromCallback(data, tenant.jwk);
319
+ } catch (error) {
320
+ throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
321
+ }
266
322
  const node = new TinyCloudNode({ host: this.#config.nodeHost });
267
- await node.restoreSession(session);
323
+ try {
324
+ await node.restoreSession(session);
325
+ } catch (error) {
326
+ throw new ApprovalStageError(
327
+ "restore_approval",
328
+ error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
329
+ void 0,
330
+ error
331
+ );
332
+ }
268
333
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
269
- throw new Error("The approved delegation targets a different session.");
334
+ throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
270
335
  }
271
336
  return { node, session };
272
337
  }
@@ -325,10 +390,10 @@ var RemoteTenantStore = class {
325
390
  return { stateRoot, sessionDid, jwk };
326
391
  }
327
392
  async #readTenant(tenantId) {
328
- if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
393
+ if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
329
394
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
330
395
  const jwk = await readJson(this.#keyPath(stateRoot));
331
- if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
396
+ if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
332
397
  return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
333
398
  }
334
399
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
@@ -358,26 +423,40 @@ var RemoteTenantStore = class {
358
423
  async #readPending(state, consume) {
359
424
  const [tenantId, nonce, signature] = state.split(".");
360
425
  if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
361
- throw new Error("Approval state is invalid.");
426
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
362
427
  }
363
428
  const expected = this.#signState(tenantId, nonce);
364
429
  const supplied = Buffer.from(signature, "base64url");
365
430
  const expectedBytes = Buffer.from(expected, "base64url");
366
431
  if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
367
- throw new Error("Approval state is invalid.");
432
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
368
433
  }
369
434
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
370
435
  const path = this.#pendingPath(stateRoot, nonce);
371
436
  const claimed = `${path}.processing`;
372
- if (consume) await rename(path, claimed);
437
+ if (consume) {
438
+ try {
439
+ await rename(path, claimed);
440
+ } catch (error) {
441
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
442
+ const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
443
+ (jwk) => jwk !== null,
444
+ () => true
445
+ );
446
+ if (!tenantExists) {
447
+ throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
448
+ }
449
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
450
+ }
451
+ }
373
452
  try {
374
453
  const pending = await readJson(consume ? claimed : path);
375
454
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
376
- throw new Error("Approval state is invalid or already used.");
455
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
377
456
  }
378
457
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
379
458
  await rm(consume ? claimed : path, { force: true });
380
- throw new Error("Approval state has expired.");
459
+ throw new ApprovalRequestError("state_expired", "Approval state has expired.");
381
460
  }
382
461
  return pending;
383
462
  } catch (error) {
@@ -824,7 +903,7 @@ function registerTinyCloudTools(server, selection, validator) {
824
903
  // package.json
825
904
  var package_default = {
826
905
  name: "@tinycloud/mcp",
827
- version: "0.5.4-beta.0",
906
+ version: "0.5.4-beta.1",
828
907
  description: "TinyCloud delegated operations MCP server",
829
908
  keywords: [
830
909
  "tinycloud",
@@ -897,7 +976,7 @@ var package_default = {
897
976
  "@modelcontextprotocol/server": "2.0.0-beta.4",
898
977
  "@tinycloud/node-sdk": "3.2.4-beta.0",
899
978
  "@tinycloud/node-sdk-wasm": "1.7.7",
900
- "@tinycloud/operations": "0.5.4-beta.0",
979
+ "@tinycloud/operations": "0.5.4-beta.1",
901
980
  jose: "6.1.3"
902
981
  },
903
982
  devDependencies: {
@@ -1000,8 +1079,23 @@ function createHostedMcpApp(config) {
1000
1079
  await store.completeApproval(requiredState(url), request);
1001
1080
  return callbackCors(json({ success: true }), config.openkeyHost);
1002
1081
  } catch (error) {
1082
+ if (error instanceof ApprovalOwnerMismatchError) {
1083
+ return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
1084
+ }
1085
+ const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
1086
+ const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
1087
+ const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
1088
+ const detail = error instanceof ApprovalRequestError ? error.detail : "none";
1089
+ writeDiagnostic(
1090
+ `connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
1091
+ );
1003
1092
  return callbackCors(
1004
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
1093
+ json({
1094
+ error: stage,
1095
+ code,
1096
+ ...reason === void 0 ? {} : { reason },
1097
+ message: "TinyCloud could not accept this delegation."
1098
+ }, 400),
1005
1099
  config.openkeyHost
1006
1100
  );
1007
1101
  }
@@ -1076,7 +1170,8 @@ function canonicalResourceUrl(url) {
1076
1170
  }
1077
1171
  function requiredState(url) {
1078
1172
  const state = url.searchParams.get("state");
1079
- if (state === null || state.length > 512) throw new Error("Missing approval state.");
1173
+ if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
1174
+ if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1080
1175
  return state;
1081
1176
  }
1082
1177
  function validateOrigin(request, allowed) {