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

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
@@ -24,6 +24,7 @@ __export(http_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(http_exports);
26
26
  var import_server4 = require("@modelcontextprotocol/server");
27
+ var import_node_sdk3 = require("@tinycloud/node-sdk");
27
28
 
28
29
  // src/oauth.ts
29
30
  var import_server = require("@modelcontextprotocol/server");
@@ -111,6 +112,7 @@ var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
111
112
  var import_operations = require("@tinycloud/operations");
112
113
  var PROFILE = "agent";
113
114
  var MAX_CALLBACK_BYTES = 256 * 1024;
115
+ var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
114
116
  var BOOTSTRAP_PERMISSIONS = [
115
117
  {
116
118
  service: "tinycloud.kv",
@@ -131,6 +133,26 @@ var ApprovalOwnerMismatchError = class extends Error {
131
133
  this.name = "ApprovalOwnerMismatchError";
132
134
  }
133
135
  };
136
+ var ApprovalStageError = class extends Error {
137
+ stage;
138
+ code;
139
+ reason;
140
+ constructor(stage, code, reason, cause) {
141
+ super(`The approval failed at ${stage} (${code}).`, { cause });
142
+ this.name = "ApprovalStageError";
143
+ this.stage = stage;
144
+ this.code = code;
145
+ this.reason = reason;
146
+ }
147
+ };
148
+ var ApprovalRequestError = class extends Error {
149
+ detail;
150
+ constructor(detail, message, cause) {
151
+ super(message, { cause });
152
+ this.name = "ApprovalRequestError";
153
+ this.detail = detail;
154
+ }
155
+ };
134
156
  var RemoteTenantStore = class {
135
157
  #config;
136
158
  #initializing = /* @__PURE__ */ new Map();
@@ -196,11 +218,20 @@ var RemoteTenantStore = class {
196
218
  }
197
219
  async completeApproval(state, request) {
198
220
  const length = Number(request.headers.get("content-length") ?? "0");
199
- if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
221
+ if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
222
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
223
+ }
200
224
  const text = await request.text();
201
- if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
202
- const data = JSON.parse(text);
203
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
225
+ if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
226
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
227
+ }
228
+ let data;
229
+ try {
230
+ data = JSON.parse(text);
231
+ } catch (error) {
232
+ throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
233
+ }
234
+ if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
204
235
  const pending = await this.#readPending(state, true);
205
236
  try {
206
237
  const tenant = await this.#readTenant(pending.tenantId);
@@ -238,16 +269,26 @@ var RemoteTenantStore = class {
238
269
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
239
270
  const { node, session } = await this.#restoreApproval(tenant, data);
240
271
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
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
- });
272
+ let delegation;
273
+ try {
274
+ delegation = (0, import_node_sdk2.siweRuntimeDelegation)(node, {
275
+ authorization: session.delegationHeader.Authorization,
276
+ cid: session.delegationCid,
277
+ spaceId: session.spaceId,
278
+ ownerAddress: session.address,
279
+ chainId: session.chainId,
280
+ siwe: session.siwe,
281
+ signature: session.signature,
282
+ host: this.#config.nodeHost
283
+ });
284
+ } catch (error) {
285
+ throw new ApprovalStageError(
286
+ "siwe_runtime_delegation",
287
+ "DELEGATION_REJECTED",
288
+ error instanceof import_node_sdk2.RuntimeDelegationRejectedError ? error.reason : void 0,
289
+ error
290
+ );
291
+ }
251
292
  const result = await (0, import_operations.invokeOperation)(
252
293
  "tinycloud.auth.import",
253
294
  1,
@@ -260,7 +301,15 @@ var RemoteTenantStore = class {
260
301
  delegation
261
302
  }
262
303
  );
263
- if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
304
+ if (result.status === "error") {
305
+ const reason = result.error.details?.reason;
306
+ throw new ApprovalStageError(
307
+ result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
308
+ result.error.code,
309
+ typeof reason === "string" ? reason : void 0
310
+ );
311
+ }
312
+ if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
264
313
  }
265
314
  /**
266
315
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
@@ -270,11 +319,25 @@ var RemoteTenantStore = class {
270
319
  * exactly the delegation being stored.
271
320
  */
272
321
  async #restoreApproval(tenant, data) {
273
- const session = sessionFromCallback(data, tenant.jwk);
322
+ let session;
323
+ try {
324
+ session = sessionFromCallback(data, tenant.jwk);
325
+ } catch (error) {
326
+ throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
327
+ }
274
328
  const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
275
- await node.restoreSession(session);
329
+ try {
330
+ await node.restoreSession(session);
331
+ } catch (error) {
332
+ throw new ApprovalStageError(
333
+ "restore_approval",
334
+ error instanceof import_node_sdk2.InvalidRestoredSessionError || error instanceof import_node_sdk2.UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
335
+ void 0,
336
+ error
337
+ );
338
+ }
276
339
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
277
- throw new Error("The approved delegation targets a different session.");
340
+ throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
278
341
  }
279
342
  return { node, session };
280
343
  }
@@ -315,7 +378,8 @@ var RemoteTenantStore = class {
315
378
  sessionDid = didFromJwk(jwk);
316
379
  }
317
380
  const profilePath = this.#profilePath(stateRoot);
318
- if (!await fileExists(profilePath)) {
381
+ const profile = await readJson(profilePath);
382
+ if (profile === null) {
319
383
  await secureWrite(profilePath, {
320
384
  name: PROFILE,
321
385
  host: this.#config.nodeHost,
@@ -329,14 +393,20 @@ var RemoteTenantStore = class {
329
393
  authMethod: "openkey",
330
394
  openkeyHost: this.#config.openkeyHost
331
395
  });
396
+ } else if (typeof profile.host === "string" && profile.host.replace(/\/+$/, "") === LEGACY_DEFAULT_NODE_HOST && this.#config.nodeHost.replace(/\/+$/, "") !== LEGACY_DEFAULT_NODE_HOST) {
397
+ await secureWrite(profilePath, { ...profile, host: this.#config.nodeHost });
398
+ process.stderr.write(
399
+ `[tinycloud-mcp-http] tenant node host migrated: from=${LEGACY_DEFAULT_NODE_HOST} to=${new URL(this.#config.nodeHost).origin}
400
+ `
401
+ );
332
402
  }
333
403
  return { stateRoot, sessionDid, jwk };
334
404
  }
335
405
  async #readTenant(tenantId) {
336
- if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
406
+ if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
337
407
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
338
408
  const jwk = await readJson(this.#keyPath(stateRoot));
339
- if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
409
+ if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
340
410
  return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
341
411
  }
342
412
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
@@ -366,26 +436,40 @@ var RemoteTenantStore = class {
366
436
  async #readPending(state, consume) {
367
437
  const [tenantId, nonce, signature] = state.split(".");
368
438
  if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
369
- throw new Error("Approval state is invalid.");
439
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
370
440
  }
371
441
  const expected = this.#signState(tenantId, nonce);
372
442
  const supplied = Buffer.from(signature, "base64url");
373
443
  const expectedBytes = Buffer.from(expected, "base64url");
374
444
  if (supplied.length !== expectedBytes.length || !(0, import_node_crypto.timingSafeEqual)(supplied, expectedBytes)) {
375
- throw new Error("Approval state is invalid.");
445
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
376
446
  }
377
447
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
378
448
  const path = this.#pendingPath(stateRoot, nonce);
379
449
  const claimed = `${path}.processing`;
380
- if (consume) await (0, import_promises.rename)(path, claimed);
450
+ if (consume) {
451
+ try {
452
+ await (0, import_promises.rename)(path, claimed);
453
+ } catch (error) {
454
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
455
+ const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
456
+ (jwk) => jwk !== null,
457
+ () => true
458
+ );
459
+ if (!tenantExists) {
460
+ throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
461
+ }
462
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
463
+ }
464
+ }
381
465
  try {
382
466
  const pending = await readJson(consume ? claimed : path);
383
467
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
384
- throw new Error("Approval state is invalid or already used.");
468
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
385
469
  }
386
470
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
387
471
  await (0, import_promises.rm)(consume ? claimed : path, { force: true });
388
- throw new Error("Approval state has expired.");
472
+ throw new ApprovalRequestError("state_expired", "Approval state has expired.");
389
473
  }
390
474
  return pending;
391
475
  } catch (error) {
@@ -508,9 +592,6 @@ async function readJson(path) {
508
592
  throw error;
509
593
  }
510
594
  }
511
- async function fileExists(path) {
512
- return await readJson(path) !== null;
513
- }
514
595
  async function reusablePending(directory, expected) {
515
596
  let names;
516
597
  try {
@@ -833,7 +914,7 @@ function registerTinyCloudTools(server, selection, validator) {
833
914
  // package.json
834
915
  var package_default = {
835
916
  name: "@tinycloud/mcp",
836
- version: "0.5.4-beta.0",
917
+ version: "0.5.4-beta.2",
837
918
  description: "TinyCloud delegated operations MCP server",
838
919
  keywords: [
839
920
  "tinycloud",
@@ -906,7 +987,7 @@ var package_default = {
906
987
  "@modelcontextprotocol/server": "2.0.0-beta.4",
907
988
  "@tinycloud/node-sdk": "3.2.4-beta.0",
908
989
  "@tinycloud/node-sdk-wasm": "1.7.7",
909
- "@tinycloud/operations": "0.5.4-beta.0",
990
+ "@tinycloud/operations": "0.5.4-beta.1",
910
991
  jose: "6.1.3"
911
992
  },
912
993
  devDependencies: {
@@ -1009,8 +1090,23 @@ function createHostedMcpApp(config) {
1009
1090
  await store.completeApproval(requiredState(url), request);
1010
1091
  return callbackCors(json({ success: true }), config.openkeyHost);
1011
1092
  } catch (error) {
1093
+ if (error instanceof ApprovalOwnerMismatchError) {
1094
+ return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
1095
+ }
1096
+ const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
1097
+ const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
1098
+ const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : import_node_sdk3.RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
1099
+ const detail = error instanceof ApprovalRequestError ? error.detail : "none";
1100
+ writeDiagnostic(
1101
+ `connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
1102
+ );
1012
1103
  return callbackCors(
1013
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
1104
+ json({
1105
+ error: stage,
1106
+ code,
1107
+ ...reason === void 0 ? {} : { reason },
1108
+ message: "TinyCloud could not accept this delegation."
1109
+ }, 400),
1014
1110
  config.openkeyHost
1015
1111
  );
1016
1112
  }
@@ -1085,7 +1181,8 @@ function canonicalResourceUrl(url) {
1085
1181
  }
1086
1182
  function requiredState(url) {
1087
1183
  const state = url.searchParams.get("state");
1088
- if (state === null || state.length > 512) throw new Error("Missing approval state.");
1184
+ if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
1185
+ if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1089
1186
  return state;
1090
1187
  }
1091
1188
  function validateOrigin(request, allowed) {