@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-cli.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  oauthMetadataResponse,
14
14
  requireBearerAuth
15
15
  } from "@modelcontextprotocol/server";
16
+ import { RUNTIME_DELEGATION_REJECTIONS } from "@tinycloud/node-sdk";
16
17
 
17
18
  // src/oauth.ts
18
19
  import {
@@ -111,7 +112,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
111
112
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
112
113
  import { dirname, join } from "path";
113
114
  import {
115
+ InvalidRestoredSessionError,
116
+ RuntimeDelegationRejectedError,
114
117
  TinyCloudNode,
118
+ UnsupportedSessionRestoreError,
115
119
  isCapabilitySubset,
116
120
  principalDidEquals,
117
121
  siweRuntimeDelegation
@@ -140,6 +144,26 @@ var ApprovalOwnerMismatchError = class extends Error {
140
144
  this.name = "ApprovalOwnerMismatchError";
141
145
  }
142
146
  };
147
+ var ApprovalStageError = class extends Error {
148
+ stage;
149
+ code;
150
+ reason;
151
+ constructor(stage, code, reason, cause) {
152
+ super(`The approval failed at ${stage} (${code}).`, { cause });
153
+ this.name = "ApprovalStageError";
154
+ this.stage = stage;
155
+ this.code = code;
156
+ this.reason = reason;
157
+ }
158
+ };
159
+ var ApprovalRequestError = class extends Error {
160
+ detail;
161
+ constructor(detail, message, cause) {
162
+ super(message, { cause });
163
+ this.name = "ApprovalRequestError";
164
+ this.detail = detail;
165
+ }
166
+ };
143
167
  var RemoteTenantStore = class {
144
168
  #config;
145
169
  #initializing = /* @__PURE__ */ new Map();
@@ -205,11 +229,20 @@ var RemoteTenantStore = class {
205
229
  }
206
230
  async completeApproval(state, request) {
207
231
  const length = Number(request.headers.get("content-length") ?? "0");
208
- if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
232
+ if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
233
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
234
+ }
209
235
  const text = await request.text();
210
- if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
211
- const data = JSON.parse(text);
212
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
236
+ if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
237
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
238
+ }
239
+ let data;
240
+ try {
241
+ data = JSON.parse(text);
242
+ } catch (error) {
243
+ throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
244
+ }
245
+ if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
213
246
  const pending = await this.#readPending(state, true);
214
247
  try {
215
248
  const tenant = await this.#readTenant(pending.tenantId);
@@ -247,16 +280,26 @@ var RemoteTenantStore = class {
247
280
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
248
281
  const { node, session } = await this.#restoreApproval(tenant, data);
249
282
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
250
- const delegation = siweRuntimeDelegation(node, {
251
- authorization: session.delegationHeader.Authorization,
252
- cid: session.delegationCid,
253
- spaceId: session.spaceId,
254
- ownerAddress: session.address,
255
- chainId: session.chainId,
256
- siwe: session.siwe,
257
- signature: session.signature,
258
- host: this.#config.nodeHost
259
- });
283
+ let delegation;
284
+ try {
285
+ delegation = siweRuntimeDelegation(node, {
286
+ authorization: session.delegationHeader.Authorization,
287
+ cid: session.delegationCid,
288
+ spaceId: session.spaceId,
289
+ ownerAddress: session.address,
290
+ chainId: session.chainId,
291
+ siwe: session.siwe,
292
+ signature: session.signature,
293
+ host: this.#config.nodeHost
294
+ });
295
+ } catch (error) {
296
+ throw new ApprovalStageError(
297
+ "siwe_runtime_delegation",
298
+ "DELEGATION_REJECTED",
299
+ error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
300
+ error
301
+ );
302
+ }
260
303
  const result = await invokeOperation(
261
304
  "tinycloud.auth.import",
262
305
  1,
@@ -269,7 +312,15 @@ var RemoteTenantStore = class {
269
312
  delegation
270
313
  }
271
314
  );
272
- if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
315
+ if (result.status === "error") {
316
+ const reason = result.error.details?.reason;
317
+ throw new ApprovalStageError(
318
+ result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
319
+ result.error.code,
320
+ typeof reason === "string" ? reason : void 0
321
+ );
322
+ }
323
+ if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
273
324
  }
274
325
  /**
275
326
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
@@ -279,11 +330,25 @@ var RemoteTenantStore = class {
279
330
  * exactly the delegation being stored.
280
331
  */
281
332
  async #restoreApproval(tenant, data) {
282
- const session = sessionFromCallback(data, tenant.jwk);
333
+ let session;
334
+ try {
335
+ session = sessionFromCallback(data, tenant.jwk);
336
+ } catch (error) {
337
+ throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
338
+ }
283
339
  const node = new TinyCloudNode({ host: this.#config.nodeHost });
284
- await node.restoreSession(session);
340
+ try {
341
+ await node.restoreSession(session);
342
+ } catch (error) {
343
+ throw new ApprovalStageError(
344
+ "restore_approval",
345
+ error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
346
+ void 0,
347
+ error
348
+ );
349
+ }
285
350
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
286
- throw new Error("The approved delegation targets a different session.");
351
+ throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
287
352
  }
288
353
  return { node, session };
289
354
  }
@@ -342,10 +407,10 @@ var RemoteTenantStore = class {
342
407
  return { stateRoot, sessionDid, jwk };
343
408
  }
344
409
  async #readTenant(tenantId) {
345
- if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
410
+ if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
346
411
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
347
412
  const jwk = await readJson(this.#keyPath(stateRoot));
348
- if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
413
+ if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
349
414
  return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
350
415
  }
351
416
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
@@ -375,26 +440,40 @@ var RemoteTenantStore = class {
375
440
  async #readPending(state, consume) {
376
441
  const [tenantId, nonce, signature] = state.split(".");
377
442
  if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
378
- throw new Error("Approval state is invalid.");
443
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
379
444
  }
380
445
  const expected = this.#signState(tenantId, nonce);
381
446
  const supplied = Buffer.from(signature, "base64url");
382
447
  const expectedBytes = Buffer.from(expected, "base64url");
383
448
  if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
384
- throw new Error("Approval state is invalid.");
449
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
385
450
  }
386
451
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
387
452
  const path = this.#pendingPath(stateRoot, nonce);
388
453
  const claimed = `${path}.processing`;
389
- if (consume) await rename(path, claimed);
454
+ if (consume) {
455
+ try {
456
+ await rename(path, claimed);
457
+ } catch (error) {
458
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
459
+ const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
460
+ (jwk) => jwk !== null,
461
+ () => true
462
+ );
463
+ if (!tenantExists) {
464
+ throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
465
+ }
466
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
467
+ }
468
+ }
390
469
  try {
391
470
  const pending = await readJson(consume ? claimed : path);
392
471
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
393
- throw new Error("Approval state is invalid or already used.");
472
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
394
473
  }
395
474
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
396
475
  await rm(consume ? claimed : path, { force: true });
397
- throw new Error("Approval state has expired.");
476
+ throw new ApprovalRequestError("state_expired", "Approval state has expired.");
398
477
  }
399
478
  return pending;
400
479
  } catch (error) {
@@ -841,7 +920,7 @@ function registerTinyCloudTools(server, selection, validator) {
841
920
  // package.json
842
921
  var package_default = {
843
922
  name: "@tinycloud/mcp",
844
- version: "0.5.4-beta.0",
923
+ version: "0.5.4-beta.1",
845
924
  description: "TinyCloud delegated operations MCP server",
846
925
  keywords: [
847
926
  "tinycloud",
@@ -914,7 +993,7 @@ var package_default = {
914
993
  "@modelcontextprotocol/server": "2.0.0-beta.4",
915
994
  "@tinycloud/node-sdk": "3.2.4-beta.0",
916
995
  "@tinycloud/node-sdk-wasm": "1.7.7",
917
- "@tinycloud/operations": "0.5.4-beta.0",
996
+ "@tinycloud/operations": "0.5.4-beta.1",
918
997
  jose: "6.1.3"
919
998
  },
920
999
  devDependencies: {
@@ -1017,8 +1096,23 @@ function createHostedMcpApp(config) {
1017
1096
  await store.completeApproval(requiredState(url), request);
1018
1097
  return callbackCors(json({ success: true }), config.openkeyHost);
1019
1098
  } catch (error) {
1099
+ if (error instanceof ApprovalOwnerMismatchError) {
1100
+ return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
1101
+ }
1102
+ const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
1103
+ const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
1104
+ const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
1105
+ const detail = error instanceof ApprovalRequestError ? error.detail : "none";
1106
+ writeDiagnostic(
1107
+ `connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
1108
+ );
1020
1109
  return callbackCors(
1021
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
1110
+ json({
1111
+ error: stage,
1112
+ code,
1113
+ ...reason === void 0 ? {} : { reason },
1114
+ message: "TinyCloud could not accept this delegation."
1115
+ }, 400),
1022
1116
  config.openkeyHost
1023
1117
  );
1024
1118
  }
@@ -1093,7 +1187,8 @@ function canonicalResourceUrl(url) {
1093
1187
  }
1094
1188
  function requiredState(url) {
1095
1189
  const state = url.searchParams.get("state");
1096
- if (state === null || state.length > 512) throw new Error("Missing approval state.");
1190
+ if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
1191
+ if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1097
1192
  return state;
1098
1193
  }
1099
1194
  function validateOrigin(request, allowed) {