@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/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +121 -29
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +124 -29
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +121 -29
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +124 -29
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +121 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +124 -29
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +2 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
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)
|
|
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)
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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) {
|