@tinycloud/mcp 0.8.3-beta.2 → 0.8.3-beta.21
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/README.md +52 -44
- package/dist/cli.cjs +4 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +940 -26
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +939 -22
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +927 -25
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +24 -0
- package/dist/http.d.ts +24 -0
- package/dist/http.js +926 -21
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +927 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +926 -21
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +4 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +3 -1
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +3 -3
- package/skills/tinycloud-delegated-secrets/SKILL.md +18 -4
package/dist/http-cli.cjs
CHANGED
|
@@ -46,7 +46,9 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
46
46
|
issuer: config.issuer,
|
|
47
47
|
audience: config.audience
|
|
48
48
|
});
|
|
49
|
-
|
|
49
|
+
const info = authInfo(token, payload, config);
|
|
50
|
+
if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
|
|
51
|
+
return info;
|
|
50
52
|
} catch (error) {
|
|
51
53
|
if (error instanceof import_jose.errors.JOSEError) {
|
|
52
54
|
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
@@ -56,6 +58,16 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
56
58
|
}
|
|
57
59
|
};
|
|
58
60
|
}
|
|
61
|
+
async function requireUsableStatus(checker, token) {
|
|
62
|
+
const status = await checker.tokenStatus(token);
|
|
63
|
+
if (status === "active") return;
|
|
64
|
+
if (status === "inactive") {
|
|
65
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
|
|
66
|
+
}
|
|
67
|
+
if (!checker.toleratesUnknown(token)) {
|
|
68
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
59
71
|
async function loadOAuthMetadata(metadataUrl) {
|
|
60
72
|
const response = await fetch(metadataUrl, {
|
|
61
73
|
headers: { accept: "application/json" },
|
|
@@ -121,6 +133,150 @@ function firstString(...values) {
|
|
|
121
133
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
122
134
|
}
|
|
123
135
|
|
|
136
|
+
// src/token-status.ts
|
|
137
|
+
var import_node_crypto = require("crypto");
|
|
138
|
+
var import_jose2 = require("jose");
|
|
139
|
+
var ACTIVE_CACHE_MS = 3e4;
|
|
140
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
141
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
142
|
+
var MAX_CACHED_TOKENS = 1e4;
|
|
143
|
+
function createTokenStatusChecker(config) {
|
|
144
|
+
const doFetch = config.fetch ?? fetch;
|
|
145
|
+
const clock = config.now ?? Date.now;
|
|
146
|
+
const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
147
|
+
`));
|
|
148
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
149
|
+
const inactive = /* @__PURE__ */ new Map();
|
|
150
|
+
const active = /* @__PURE__ */ new Map();
|
|
151
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
152
|
+
const confirmedActive = /* @__PURE__ */ new Map();
|
|
153
|
+
let credentialsRefused = false;
|
|
154
|
+
async function introspect(token) {
|
|
155
|
+
const headers = {
|
|
156
|
+
accept: "application/json",
|
|
157
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
158
|
+
};
|
|
159
|
+
const body = new URLSearchParams({ token, token_type_hint: "access_token" });
|
|
160
|
+
if (config.authMethod === "basic") {
|
|
161
|
+
const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
162
|
+
const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
|
|
163
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
164
|
+
} else {
|
|
165
|
+
body.set("client_id", config.clientId);
|
|
166
|
+
body.set("client_secret", config.clientSecret);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const response = await doFetch(config.endpoint, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers,
|
|
172
|
+
body,
|
|
173
|
+
redirect: "error",
|
|
174
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
175
|
+
});
|
|
176
|
+
if (response.status === 401) return "credentials";
|
|
177
|
+
if (response.status !== 200) return "unknown";
|
|
178
|
+
const result = await response.json();
|
|
179
|
+
if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
|
|
180
|
+
if (result.active === false) return "inactive";
|
|
181
|
+
if (result.active !== true) return "unknown";
|
|
182
|
+
return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
|
|
183
|
+
} catch {
|
|
184
|
+
return "unknown";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function query(key, token) {
|
|
188
|
+
const outcome = await introspect(token);
|
|
189
|
+
if (outcome === "credentials") {
|
|
190
|
+
if (!credentialsRefused) {
|
|
191
|
+
log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
|
|
192
|
+
}
|
|
193
|
+
credentialsRefused = true;
|
|
194
|
+
return "unknown";
|
|
195
|
+
}
|
|
196
|
+
if (outcome === "unknown") return outcome;
|
|
197
|
+
const now = clock();
|
|
198
|
+
prune(now);
|
|
199
|
+
if (outcome === "mismatch") {
|
|
200
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
201
|
+
confirmedActive.delete(key);
|
|
202
|
+
return "inactive";
|
|
203
|
+
}
|
|
204
|
+
credentialsRefused = false;
|
|
205
|
+
if (outcome === "inactive") {
|
|
206
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
207
|
+
confirmedActive.delete(key);
|
|
208
|
+
} else {
|
|
209
|
+
active.set(key, now + ACTIVE_CACHE_MS);
|
|
210
|
+
confirmedActive.delete(key);
|
|
211
|
+
confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
|
|
212
|
+
}
|
|
213
|
+
return outcome;
|
|
214
|
+
}
|
|
215
|
+
function prune(now) {
|
|
216
|
+
for (const [key, until] of active) if (until <= now) active.delete(key);
|
|
217
|
+
for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
|
|
218
|
+
for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
|
|
219
|
+
for (const cache of [active, inactive, confirmedActive]) {
|
|
220
|
+
while (cache.size >= MAX_CACHED_TOKENS) {
|
|
221
|
+
const oldest = cache.keys().next().value;
|
|
222
|
+
if (oldest === void 0) break;
|
|
223
|
+
cache.delete(oldest);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
async tokenStatus(token) {
|
|
229
|
+
const key = (0, import_node_crypto.createHash)("sha256").update(token).digest("hex");
|
|
230
|
+
const now = clock();
|
|
231
|
+
const inactiveUntil = inactive.get(key);
|
|
232
|
+
if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
|
|
233
|
+
const activeUntil = active.get(key);
|
|
234
|
+
if (activeUntil !== void 0 && activeUntil > now) return "active";
|
|
235
|
+
const pending = inFlight.get(key);
|
|
236
|
+
if (pending !== void 0) return pending;
|
|
237
|
+
const asking = query(key, token).finally(() => inFlight.delete(key));
|
|
238
|
+
inFlight.set(key, asking);
|
|
239
|
+
return asking;
|
|
240
|
+
},
|
|
241
|
+
toleratesUnknown(token) {
|
|
242
|
+
if (credentialsRefused) return false;
|
|
243
|
+
const confirmed = confirmedActive.get((0, import_node_crypto.createHash)("sha256").update(token).digest("hex"));
|
|
244
|
+
if (confirmed === void 0) return false;
|
|
245
|
+
const now = clock();
|
|
246
|
+
return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function answerMatchesToken(answer, token, resourceUrl) {
|
|
251
|
+
const jwt = claims(token);
|
|
252
|
+
if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
|
|
253
|
+
const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
|
|
254
|
+
if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
|
|
255
|
+
if (jwt.aud === void 0) return true;
|
|
256
|
+
const answered = audienceList(answer.aud);
|
|
257
|
+
const signed = audienceList(jwt.aud);
|
|
258
|
+
return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
|
|
259
|
+
}
|
|
260
|
+
function audienceList(value) {
|
|
261
|
+
const list = typeof value === "string" ? [value] : value;
|
|
262
|
+
return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
|
|
263
|
+
}
|
|
264
|
+
function tokenExpiryMs(token, now) {
|
|
265
|
+
const exp = claims(token).exp;
|
|
266
|
+
return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
|
|
267
|
+
}
|
|
268
|
+
function tokenLifetimeSeconds(token) {
|
|
269
|
+
const { exp, iat } = claims(token);
|
|
270
|
+
return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
271
|
+
}
|
|
272
|
+
function claims(token) {
|
|
273
|
+
try {
|
|
274
|
+
return (0, import_jose2.decodeJwt)(token);
|
|
275
|
+
} catch {
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
124
280
|
// src/logging.ts
|
|
125
281
|
function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
126
282
|
process.stderr.write(`${JSON.stringify({
|
|
@@ -133,16 +289,85 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
|
133
289
|
}
|
|
134
290
|
|
|
135
291
|
// src/remote-store.ts
|
|
136
|
-
var
|
|
292
|
+
var import_node_crypto3 = require("crypto");
|
|
137
293
|
var import_promises = require("fs/promises");
|
|
138
294
|
var import_node_path = require("path");
|
|
139
295
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
140
296
|
var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
|
|
297
|
+
var import_artifacts = require("@tinycloud/operations/artifacts");
|
|
298
|
+
var import_state = require("@tinycloud/operations/state");
|
|
141
299
|
var import_operations = require("@tinycloud/operations");
|
|
142
300
|
var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
|
|
143
301
|
var import_tc_apps = require("@tinycloud/operations/tc-apps");
|
|
302
|
+
|
|
303
|
+
// src/native-auth.ts
|
|
304
|
+
var import_node_crypto2 = require("crypto");
|
|
305
|
+
var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
|
|
306
|
+
var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
|
|
307
|
+
var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
308
|
+
var NATIVE_GRANT_REQUIRED = "native_grant_required";
|
|
309
|
+
var NATIVE_GRANT_EXPIRED = "native_grant_expired";
|
|
310
|
+
var NATIVE_GRANT_REJECTED = "native_grant_rejected";
|
|
311
|
+
var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
|
|
312
|
+
var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
|
|
313
|
+
var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
|
|
314
|
+
var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
|
|
315
|
+
function serviceCredentialMatches(supplied, configured) {
|
|
316
|
+
if (supplied === null || configured === void 0) return false;
|
|
317
|
+
const suppliedBytes = Buffer.from(supplied, "utf8");
|
|
318
|
+
const configuredBytes = Buffer.from(configured, "utf8");
|
|
319
|
+
return suppliedBytes.byteLength === configuredBytes.byteLength && (0, import_node_crypto2.timingSafeEqual)(suppliedBytes, configuredBytes);
|
|
320
|
+
}
|
|
321
|
+
var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
|
|
322
|
+
var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
|
|
323
|
+
var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
|
|
324
|
+
function canonicalOwnerDid(value) {
|
|
325
|
+
const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
|
|
326
|
+
if (!match) return void 0;
|
|
327
|
+
const chainId = Number(match[1]);
|
|
328
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
329
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
330
|
+
}
|
|
331
|
+
function nativeOwnerForConnectorGrant(permissions) {
|
|
332
|
+
if (permissions.length !== 2) return void 0;
|
|
333
|
+
const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
|
|
334
|
+
const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
|
|
335
|
+
if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
|
|
336
|
+
const sql = sqlEntries[0];
|
|
337
|
+
const kv = kvEntries[0];
|
|
338
|
+
if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
|
|
339
|
+
if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
|
|
340
|
+
if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
|
|
341
|
+
const sqlSpace = ownerFromApplicationsSpace(sql.space);
|
|
342
|
+
const kvSpace = ownerFromApplicationsSpace(kv.space);
|
|
343
|
+
return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
|
|
344
|
+
}
|
|
345
|
+
function ownerFromApplicationsSpace(space) {
|
|
346
|
+
const match = OWNER_APPLICATIONS_SPACE.exec(space);
|
|
347
|
+
if (!match) return void 0;
|
|
348
|
+
const chainId = Number(match[1]);
|
|
349
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
350
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
351
|
+
}
|
|
352
|
+
function actionsEqual(actual, expected) {
|
|
353
|
+
const actualActions = [...actual].sort();
|
|
354
|
+
const expectedActions = [...expected].sort();
|
|
355
|
+
return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
|
|
356
|
+
}
|
|
357
|
+
function hasCaveats(permission) {
|
|
358
|
+
return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/remote-store.ts
|
|
144
362
|
var PROFILE = "agent";
|
|
145
363
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
364
|
+
var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
365
|
+
"DELEGATION_ARTIFACT_INVALID",
|
|
366
|
+
"DELEGATION_EXPIRED",
|
|
367
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
368
|
+
"DELEGATION_HOST_MISMATCH",
|
|
369
|
+
"DELEGATION_REJECTED"
|
|
370
|
+
]);
|
|
146
371
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
147
372
|
var BOOTSTRAP_PERMISSIONS = [
|
|
148
373
|
{
|
|
@@ -184,6 +409,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
184
409
|
this.detail = detail;
|
|
185
410
|
}
|
|
186
411
|
};
|
|
412
|
+
var NativeGrantValidationError = class extends Error {
|
|
413
|
+
code;
|
|
414
|
+
constructor(status) {
|
|
415
|
+
const code = status.startsWith("native_") ? status : status === "required" ? "native_grant_required" : status === "expired" ? "native_grant_expired" : status === "unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected";
|
|
416
|
+
super(code);
|
|
417
|
+
this.name = "NativeGrantValidationError";
|
|
418
|
+
this.code = code;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
187
421
|
var RemoteTenantStore = class {
|
|
188
422
|
#config;
|
|
189
423
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -196,9 +430,186 @@ var RemoteTenantStore = class {
|
|
|
196
430
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
197
431
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
198
432
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
433
|
+
#nativeIdentity;
|
|
434
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
199
435
|
constructor(config) {
|
|
200
436
|
this.#config = config;
|
|
201
437
|
}
|
|
438
|
+
async nativeServiceIdentity() {
|
|
439
|
+
const identity = await this.#loadNativeServiceKey();
|
|
440
|
+
return { serviceDid: normalizeDid(identity.did) };
|
|
441
|
+
}
|
|
442
|
+
nativeTenantStateRoot(ownerDid) {
|
|
443
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
444
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
445
|
+
const tenant = (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
|
|
446
|
+
return (0, import_node_path.join)(this.#config.stateDir, "native-tenants", tenant);
|
|
447
|
+
}
|
|
448
|
+
async acceptNativeGrant(serializedDelegation) {
|
|
449
|
+
if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
|
|
450
|
+
throw new NativeGrantValidationError("rejected");
|
|
451
|
+
}
|
|
452
|
+
const identity = await this.#loadNativeServiceKey();
|
|
453
|
+
const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
|
|
454
|
+
const { ownerDid, spaceId, permissions, expiresAt } = activated;
|
|
455
|
+
return this.#withNativeGrantLock(ownerDid, async () => {
|
|
456
|
+
const stateRoot = this.nativeTenantStateRoot(ownerDid);
|
|
457
|
+
let previous = null;
|
|
458
|
+
try {
|
|
459
|
+
const stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
460
|
+
if (isRecord(stored) && stored.version === 1 && stored.ownerDid === ownerDid && typeof stored.serviceDid === "string" && samePrincipal(stored.serviceDid, identity.did) && typeof stored.serializedDelegation === "string" && typeof stored.delegationCid === "string" && typeof stored.spaceId === "string" && Array.isArray(stored.permissions) && stored.permissions.every(isPermissionEntry) && nativeOwnerForConnectorGrant(stored.permissions) === ownerDid && typeof stored.expiresAt === "string" && Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
461
|
+
previous = stored;
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
465
|
+
}
|
|
466
|
+
if (previous !== null) {
|
|
467
|
+
const previousExpiry = Date.parse(previous.expiresAt);
|
|
468
|
+
const nextExpiry = expiresAt.getTime();
|
|
469
|
+
if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
|
|
470
|
+
throw new NativeGrantValidationError("rejected");
|
|
471
|
+
}
|
|
472
|
+
if (previous.delegationCid === activated.cid) {
|
|
473
|
+
return {
|
|
474
|
+
ownerDid,
|
|
475
|
+
serviceDid: normalizeDid(identity.did),
|
|
476
|
+
expiresAt: previous.expiresAt,
|
|
477
|
+
stateRoot
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const session = {
|
|
482
|
+
delegationHeader: activated.delegation.delegationHeader,
|
|
483
|
+
delegationCid: activated.cid,
|
|
484
|
+
spaceId,
|
|
485
|
+
jwk: identity.jwk,
|
|
486
|
+
verificationMethod: identity.did,
|
|
487
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
488
|
+
};
|
|
489
|
+
const grant = {
|
|
490
|
+
version: 1,
|
|
491
|
+
ownerDid,
|
|
492
|
+
serviceDid: normalizeDid(identity.did),
|
|
493
|
+
serializedDelegation: (0, import_node_sdk2.serializeDelegation)(activated.delegation),
|
|
494
|
+
delegationCid: activated.cid,
|
|
495
|
+
spaceId,
|
|
496
|
+
permissions,
|
|
497
|
+
expiresAt: expiresAt.toISOString()
|
|
498
|
+
};
|
|
499
|
+
const sessionPath = this.#sessionPath(stateRoot);
|
|
500
|
+
const previousSession = await readOptionalText(sessionPath);
|
|
501
|
+
try {
|
|
502
|
+
await secureWrite(this.#profilePath(stateRoot), {
|
|
503
|
+
name: PROFILE,
|
|
504
|
+
host: this.#config.nodeHost,
|
|
505
|
+
chainId: activated.delegation.chainId,
|
|
506
|
+
spaceName: "applications",
|
|
507
|
+
did: identity.did,
|
|
508
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
509
|
+
profile: PROFILE,
|
|
510
|
+
operatorType: "agent",
|
|
511
|
+
posture: "delegate-session",
|
|
512
|
+
ownerDid,
|
|
513
|
+
sessionDid: identity.did,
|
|
514
|
+
spaceId,
|
|
515
|
+
tinycloudHost: this.#config.nodeHost
|
|
516
|
+
});
|
|
517
|
+
await secureWrite(this.#keyPath(stateRoot), identity.jwk);
|
|
518
|
+
await secureWrite(sessionPath, session);
|
|
519
|
+
const request = await (0, import_state.withTinyCloudStateRoot)(stateRoot, () => (0, import_artifacts.createOrReusePermissionRequest)({
|
|
520
|
+
profile: PROFILE,
|
|
521
|
+
posture: "delegate-session",
|
|
522
|
+
operatorType: "agent",
|
|
523
|
+
host: this.#config.nodeHost,
|
|
524
|
+
sessionDid: identity.did,
|
|
525
|
+
ownerDid,
|
|
526
|
+
spaceId,
|
|
527
|
+
missing: permissions,
|
|
528
|
+
granted: [],
|
|
529
|
+
replace: true
|
|
530
|
+
}));
|
|
531
|
+
if (request.status !== "created") {
|
|
532
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
533
|
+
}
|
|
534
|
+
const imported = await (0, import_operations.invokeOperation)(
|
|
535
|
+
"tinycloud.auth.import",
|
|
536
|
+
1,
|
|
537
|
+
{ profile: PROFILE, stateRoot },
|
|
538
|
+
{
|
|
539
|
+
kind: "tinycloud.auth.delegation",
|
|
540
|
+
version: 1,
|
|
541
|
+
requestId: request.request.requestId,
|
|
542
|
+
delegationCid: activated.cid,
|
|
543
|
+
delegation: activated.delegation
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
if (imported.status === "error") {
|
|
547
|
+
throw new NativeGrantValidationError(
|
|
548
|
+
NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
|
|
552
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
553
|
+
}
|
|
554
|
+
await secureWrite(this.#nativeGrantPath(stateRoot), grant);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
try {
|
|
557
|
+
await restoreOptionalText(sessionPath, previousSession);
|
|
558
|
+
} catch {
|
|
559
|
+
await (0, import_promises.rm)(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
|
|
560
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
561
|
+
}
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async nativeGrantForOwner(ownerDid) {
|
|
568
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
569
|
+
if (canonical === void 0) return { status: "rejected" };
|
|
570
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
571
|
+
let stored;
|
|
572
|
+
try {
|
|
573
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
574
|
+
} catch {
|
|
575
|
+
return { status: "rejected" };
|
|
576
|
+
}
|
|
577
|
+
if (stored === null) return { status: "required" };
|
|
578
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.serviceDid !== "string" || typeof stored.serializedDelegation !== "string" || typeof stored.delegationCid !== "string" || typeof stored.spaceId !== "string" || typeof stored.expiresAt !== "string" || !Array.isArray(stored.permissions) || !stored.permissions.every(isPermissionEntry) || nativeOwnerForConnectorGrant(stored.permissions) !== canonical) {
|
|
579
|
+
return { status: "rejected" };
|
|
580
|
+
}
|
|
581
|
+
const expires = Date.parse(stored.expiresAt);
|
|
582
|
+
if (!Number.isFinite(expires)) return { status: "rejected" };
|
|
583
|
+
if (expires <= Date.now()) return { status: "expired" };
|
|
584
|
+
try {
|
|
585
|
+
const identity = await this.#loadNativeServiceKey();
|
|
586
|
+
if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
|
|
587
|
+
const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
|
|
588
|
+
if (activated.ownerDid !== canonical || activated.cid !== stored.delegationCid || activated.spaceId !== stored.spaceId || JSON.stringify(activated.permissions) !== JSON.stringify(stored.permissions) || activated.expiresAt.toISOString() !== stored.expiresAt) return { status: "rejected" };
|
|
589
|
+
return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (error instanceof NativeGrantValidationError) {
|
|
592
|
+
return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
|
|
593
|
+
}
|
|
594
|
+
return { status: "unavailable" };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async assertNativeGrantActive(ownerDid) {
|
|
598
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
599
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
600
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
601
|
+
let stored;
|
|
602
|
+
try {
|
|
603
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
604
|
+
} catch {
|
|
605
|
+
throw new NativeGrantValidationError("rejected");
|
|
606
|
+
}
|
|
607
|
+
if (stored === null) throw new NativeGrantValidationError("required");
|
|
608
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
609
|
+
throw new NativeGrantValidationError("rejected");
|
|
610
|
+
}
|
|
611
|
+
if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
|
|
612
|
+
}
|
|
202
613
|
tenantStateRoot(subject) {
|
|
203
614
|
return (0, import_node_path.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
204
615
|
}
|
|
@@ -725,7 +1136,7 @@ var RemoteTenantStore = class {
|
|
|
725
1136
|
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
726
1137
|
);
|
|
727
1138
|
if (reusable !== void 0) return reusable;
|
|
728
|
-
const nonce = (0,
|
|
1139
|
+
const nonce = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
|
|
729
1140
|
const created = Date.now();
|
|
730
1141
|
const pending = {
|
|
731
1142
|
version: 1,
|
|
@@ -750,7 +1161,7 @@ var RemoteTenantStore = class {
|
|
|
750
1161
|
const expected = this.#signState(tenantId, nonce);
|
|
751
1162
|
const supplied = Buffer.from(signature, "base64url");
|
|
752
1163
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
753
|
-
if (supplied.length !== expectedBytes.length || !(0,
|
|
1164
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto3.timingSafeEqual)(supplied, expectedBytes)) {
|
|
754
1165
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
755
1166
|
}
|
|
756
1167
|
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -801,11 +1212,128 @@ var RemoteTenantStore = class {
|
|
|
801
1212
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
802
1213
|
return url.toString();
|
|
803
1214
|
}
|
|
1215
|
+
async #withNativeGrantLock(ownerDid, action) {
|
|
1216
|
+
const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
|
|
1217
|
+
let releaseNext;
|
|
1218
|
+
const next = new Promise((resolve2) => {
|
|
1219
|
+
releaseNext = resolve2;
|
|
1220
|
+
});
|
|
1221
|
+
this.#nativeGrantQueues.set(ownerDid, next);
|
|
1222
|
+
await previous;
|
|
1223
|
+
try {
|
|
1224
|
+
return await action();
|
|
1225
|
+
} finally {
|
|
1226
|
+
releaseNext();
|
|
1227
|
+
if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async #loadNativeServiceKey() {
|
|
1231
|
+
if (this.#nativeIdentity === void 0) {
|
|
1232
|
+
const loading = this.#readOrCreateNativeServiceKey();
|
|
1233
|
+
this.#nativeIdentity = loading;
|
|
1234
|
+
void loading.catch(() => {
|
|
1235
|
+
if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
return this.#nativeIdentity;
|
|
1239
|
+
}
|
|
1240
|
+
async #readOrCreateNativeServiceKey() {
|
|
1241
|
+
const path = (0, import_node_path.join)(this.#config.stateDir, ".tinycloud", "native-service-key.json");
|
|
1242
|
+
await secureDirectory((0, import_node_path.dirname)(path));
|
|
1243
|
+
let stored = await readJson(path);
|
|
1244
|
+
if (stored === null) {
|
|
1245
|
+
const generated = generateSessionKey();
|
|
1246
|
+
try {
|
|
1247
|
+
await (0, import_promises.writeFile)(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
|
|
1248
|
+
`, {
|
|
1249
|
+
mode: 384,
|
|
1250
|
+
flag: "wx"
|
|
1251
|
+
});
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
if (!isRecord(error) || error.code !== "EEXIST") throw error;
|
|
1254
|
+
}
|
|
1255
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1256
|
+
stored = await readJson(path);
|
|
1257
|
+
}
|
|
1258
|
+
if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
|
|
1259
|
+
throw new Error("The native MCP service identity is unavailable.");
|
|
1260
|
+
}
|
|
1261
|
+
return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
|
|
1262
|
+
}
|
|
1263
|
+
async #activateNativeDelegation(serialized, identity) {
|
|
1264
|
+
let delegation;
|
|
1265
|
+
try {
|
|
1266
|
+
const parsed = JSON.parse(serialized);
|
|
1267
|
+
if (!isRecord(parsed)) throw new Error("invalid delegation");
|
|
1268
|
+
delegation = (0, import_node_sdk2.deserializeDelegation)(serialized);
|
|
1269
|
+
} catch {
|
|
1270
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1271
|
+
}
|
|
1272
|
+
if (!isRecord(delegation.delegationHeader) || typeof delegation.delegationHeader.Authorization !== "string" || delegation.delegationHeader.Authorization.length === 0 || typeof delegation.cid !== "string" || delegation.cid.length === 0 || typeof delegation.delegateDID !== "string" || !samePrincipal(delegation.delegateDID, normalizeDid(identity.did)) || typeof delegation.spaceId !== "string" || typeof delegation.ownerAddress !== "string" || !Number.isSafeInteger(delegation.chainId) || typeof delegation.host !== "string" || !sameNodeHost(delegation.host, this.#config.nodeHost) || !(delegation.expiry instanceof Date) || !Number.isFinite(delegation.expiry.getTime())) {
|
|
1273
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1274
|
+
}
|
|
1275
|
+
const now = Date.now();
|
|
1276
|
+
const claimedExpiry = delegation.expiry.getTime();
|
|
1277
|
+
if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
|
|
1278
|
+
if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1279
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1280
|
+
}
|
|
1281
|
+
let ownerDid;
|
|
1282
|
+
let spaceId;
|
|
1283
|
+
const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
|
|
1284
|
+
try {
|
|
1285
|
+
await node.restoreSession({
|
|
1286
|
+
delegationHeader: delegation.delegationHeader,
|
|
1287
|
+
delegationCid: delegation.cid,
|
|
1288
|
+
spaceId: delegation.spaceId,
|
|
1289
|
+
jwk: identity.jwk,
|
|
1290
|
+
verificationMethod: identity.did,
|
|
1291
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1292
|
+
});
|
|
1293
|
+
const activated = await (0, import_node_sdk2.activateValidatedRuntimeDelegation)(node, delegation, {
|
|
1294
|
+
host: this.#config.nodeHost,
|
|
1295
|
+
authorize: (permissions) => {
|
|
1296
|
+
const derivedOwner = nativeOwnerForConnectorGrant(permissions);
|
|
1297
|
+
const signedSpace = permissions[0]?.space;
|
|
1298
|
+
if (derivedOwner === void 0 || signedSpace === void 0) return false;
|
|
1299
|
+
const metadataOwner = canonicalOwnerDid(
|
|
1300
|
+
`did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
|
|
1301
|
+
);
|
|
1302
|
+
if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
|
|
1303
|
+
ownerDid = derivedOwner;
|
|
1304
|
+
spaceId = signedSpace;
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1309
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
ownerDid,
|
|
1313
|
+
spaceId,
|
|
1314
|
+
permissions: [...activated.effectivePermissions],
|
|
1315
|
+
cid: activated.cid,
|
|
1316
|
+
expiresAt: activated.expiry,
|
|
1317
|
+
delegation: activated.delegation
|
|
1318
|
+
};
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
if (error instanceof NativeGrantValidationError) throw error;
|
|
1321
|
+
if (error instanceof import_node_sdk2.RuntimeDelegationRejectedError) {
|
|
1322
|
+
throw new NativeGrantValidationError(
|
|
1323
|
+
error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
#nativeGrantPath(stateRoot) {
|
|
1330
|
+
return (0, import_node_path.join)(stateRoot, ".tinycloud", "native-grant.json");
|
|
1331
|
+
}
|
|
804
1332
|
#tenantId(subject) {
|
|
805
|
-
return (0,
|
|
1333
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
806
1334
|
}
|
|
807
1335
|
#signState(tenantId, nonce) {
|
|
808
|
-
return (0,
|
|
1336
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
809
1337
|
}
|
|
810
1338
|
#profilePath(stateRoot) {
|
|
811
1339
|
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
@@ -894,6 +1422,21 @@ function samePrincipal(left, right) {
|
|
|
894
1422
|
function normalizeDid(value) {
|
|
895
1423
|
return value.split("#", 1)[0];
|
|
896
1424
|
}
|
|
1425
|
+
function sameNodeHost(left, right) {
|
|
1426
|
+
try {
|
|
1427
|
+
const normalize = (value) => {
|
|
1428
|
+
const url = new URL(value);
|
|
1429
|
+
if (url.username !== "" || url.password !== "") return "";
|
|
1430
|
+
url.hash = "";
|
|
1431
|
+
url.search = "";
|
|
1432
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
1433
|
+
return url.toString().replace(/\/+$/, "");
|
|
1434
|
+
};
|
|
1435
|
+
return normalize(left) !== "" && normalize(left) === normalize(right);
|
|
1436
|
+
} catch {
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
897
1440
|
function isPermissionEntry(value) {
|
|
898
1441
|
return isRecord(value) && typeof value.service === "string" && typeof value.space === "string" && typeof value.path === "string" && Array.isArray(value.actions) && value.actions.every((action) => typeof action === "string");
|
|
899
1442
|
}
|
|
@@ -907,7 +1450,7 @@ async function secureDirectory(path) {
|
|
|
907
1450
|
async function secureWrite(path, value) {
|
|
908
1451
|
const directory = (0, import_node_path.dirname)(path);
|
|
909
1452
|
await secureDirectory(directory);
|
|
910
|
-
const temporary = `${path}.${process.pid}.${(0,
|
|
1453
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`;
|
|
911
1454
|
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
912
1455
|
`, { mode: 384 });
|
|
913
1456
|
await (0, import_promises.rename)(temporary, path);
|
|
@@ -924,6 +1467,23 @@ async function readJson(path) {
|
|
|
924
1467
|
throw error;
|
|
925
1468
|
}
|
|
926
1469
|
}
|
|
1470
|
+
async function readOptionalText(path) {
|
|
1471
|
+
try {
|
|
1472
|
+
return await (0, import_promises.readFile)(path, "utf8");
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
1475
|
+
throw error;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
async function restoreOptionalText(path, contents) {
|
|
1479
|
+
if (contents === null) {
|
|
1480
|
+
await (0, import_promises.rm)(path, { force: true });
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
await secureDirectory((0, import_node_path.dirname)(path));
|
|
1484
|
+
await (0, import_promises.writeFile)(path, contents, { mode: 384 });
|
|
1485
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1486
|
+
}
|
|
927
1487
|
async function reusablePending(directory, expected) {
|
|
928
1488
|
let names;
|
|
929
1489
|
try {
|
|
@@ -1321,7 +1881,7 @@ function createJsonSchemaValidator() {
|
|
|
1321
1881
|
return new import_ajv.AjvJsonSchemaValidator();
|
|
1322
1882
|
}
|
|
1323
1883
|
function registerTinyCloudTools(server, selection, validator) {
|
|
1324
|
-
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
|
|
1884
|
+
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
|
|
1325
1885
|
for (const binding of bindings) {
|
|
1326
1886
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
1327
1887
|
if (operation === void 0) {
|
|
@@ -1399,7 +1959,7 @@ function outcomeCode(result) {
|
|
|
1399
1959
|
// package.json
|
|
1400
1960
|
var package_default = {
|
|
1401
1961
|
name: "@tinycloud/mcp",
|
|
1402
|
-
version: "0.8.3-beta.
|
|
1962
|
+
version: "0.8.3-beta.21",
|
|
1403
1963
|
description: "TinyCloud delegated operations MCP server",
|
|
1404
1964
|
keywords: [
|
|
1405
1965
|
"tinycloud",
|
|
@@ -1470,9 +2030,9 @@ var package_default = {
|
|
|
1470
2030
|
dependencies: {
|
|
1471
2031
|
"@modelcontextprotocol/node": "2.0.0-beta.5",
|
|
1472
2032
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
1473
|
-
"@tinycloud/node-sdk": "3.8.0-beta.
|
|
2033
|
+
"@tinycloud/node-sdk": "3.8.0-beta.19",
|
|
1474
2034
|
"@tinycloud/node-sdk-wasm": "1.7.8",
|
|
1475
|
-
"@tinycloud/operations": "0.7.7-beta.
|
|
2035
|
+
"@tinycloud/operations": "0.7.7-beta.18",
|
|
1476
2036
|
jose: "6.2.4"
|
|
1477
2037
|
},
|
|
1478
2038
|
devDependencies: {
|
|
@@ -1496,15 +2056,22 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
|
|
|
1496
2056
|
|
|
1497
2057
|
// src/http.ts
|
|
1498
2058
|
var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
2059
|
+
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
2060
|
+
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1499
2061
|
function createHostedMcpApp(config) {
|
|
2062
|
+
if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
|
|
2063
|
+
throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
|
|
2064
|
+
}
|
|
1500
2065
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1501
2066
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1502
|
-
const
|
|
2067
|
+
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
2068
|
+
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1503
2069
|
const plugin = createHostedSurface({
|
|
1504
2070
|
...config,
|
|
2071
|
+
nativeServiceCredential: void 0,
|
|
1505
2072
|
stateDir: (0, import_node_path3.join)(config.stateDir, "chatgpt"),
|
|
1506
2073
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1507
|
-
}, pluginUrl, "public");
|
|
2074
|
+
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
1508
2075
|
const pluginPaths = {
|
|
1509
2076
|
[pluginUrl.pathname]: true,
|
|
1510
2077
|
[new URL("./connect", pluginUrl).pathname]: true,
|
|
@@ -1518,7 +2085,7 @@ function createHostedMcpApp(config) {
|
|
|
1518
2085
|
}
|
|
1519
2086
|
};
|
|
1520
2087
|
}
|
|
1521
|
-
function createHostedSurface(config, resourceUrl, surface) {
|
|
2088
|
+
function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
1522
2089
|
const connectPath = config.approvalPath ?? "/connect";
|
|
1523
2090
|
const callbackPath = `${connectPath}/callback`;
|
|
1524
2091
|
const issuer = String(config.oauthMetadata.issuer);
|
|
@@ -1527,7 +2094,8 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1527
2094
|
issuer,
|
|
1528
2095
|
audience: resourceUrl.toString(),
|
|
1529
2096
|
jwksUri,
|
|
1530
|
-
requiredScope: REQUIRED_SCOPE
|
|
2097
|
+
requiredScope: REQUIRED_SCOPE,
|
|
2098
|
+
...tokenStatus === void 0 ? {} : { tokenStatus }
|
|
1531
2099
|
});
|
|
1532
2100
|
const resourceMetadataUrl = (0, import_server4.getOAuthProtectedResourceMetadataUrl)(resourceUrl);
|
|
1533
2101
|
const authenticate = (0, import_server4.requireBearerAuth)({
|
|
@@ -1536,16 +2104,19 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1536
2104
|
resourceMetadataUrl
|
|
1537
2105
|
});
|
|
1538
2106
|
const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
|
|
2107
|
+
const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
|
|
2108
|
+
if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
|
|
1539
2109
|
const handler = (0, import_server4.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
|
|
1540
|
-
const
|
|
2110
|
+
const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
|
|
2111
|
+
const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
|
|
1541
2112
|
const { subject, ownerDids } = identity;
|
|
1542
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2113
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1543
2114
|
const validator = createJsonSchemaValidator();
|
|
1544
2115
|
const server = new import_server4.McpServer(
|
|
1545
2116
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1546
2117
|
{
|
|
1547
2118
|
jsonSchemaValidator: validator,
|
|
1548
|
-
instructions: surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first."
|
|
2119
|
+
instructions: nativeOwner === void 0 ? surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first." : "Use only the user's requested connector-transcript data. The signed TinyCloud grant authorizes read-only SQL and transcript KV reads."
|
|
1549
2120
|
}
|
|
1550
2121
|
);
|
|
1551
2122
|
registerTinyCloudTools(server, {
|
|
@@ -1554,10 +2125,15 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1554
2125
|
explicitProfile: true,
|
|
1555
2126
|
allowOwnerProfile: false,
|
|
1556
2127
|
stateRoot,
|
|
1557
|
-
|
|
1558
|
-
|
|
2128
|
+
...nativeOwner === void 0 ? {} : {
|
|
2129
|
+
allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
|
|
2130
|
+
},
|
|
2131
|
+
beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
|
|
2132
|
+
...nativeOwner === void 0 ? {
|
|
2133
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
2134
|
+
} : {}
|
|
1559
2135
|
}, validator);
|
|
1560
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2136
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1561
2137
|
return server;
|
|
1562
2138
|
}, {
|
|
1563
2139
|
legacy: "stateless",
|
|
@@ -1573,14 +2149,205 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1573
2149
|
const allowedHosts = [resourceUrl.hostname];
|
|
1574
2150
|
return {
|
|
1575
2151
|
async fetch(request) {
|
|
1576
|
-
const metadata = (0, import_server4.oauthMetadataResponse)(request, metadataOptions);
|
|
1577
|
-
if (metadata !== void 0) return metadata;
|
|
1578
2152
|
const url = new URL(request.url);
|
|
2153
|
+
if (url.pathname !== "/healthz") {
|
|
2154
|
+
const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
|
|
2155
|
+
if (hostFailure !== void 0) return hostFailure;
|
|
2156
|
+
}
|
|
2157
|
+
if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
|
|
2158
|
+
const metadataUrl = new URL((0, import_server4.getOAuthProtectedResourceMetadataUrl)(resourceUrl));
|
|
2159
|
+
const metadataRequest = new Request(metadataUrl.toString(), {
|
|
2160
|
+
method: request.method,
|
|
2161
|
+
headers: request.headers
|
|
2162
|
+
});
|
|
2163
|
+
const response = (0, import_server4.oauthMetadataResponse)(metadataRequest, metadataOptions);
|
|
2164
|
+
if (response !== void 0) return withBearerMethodsSupported(response);
|
|
2165
|
+
}
|
|
2166
|
+
const metadata = (0, import_server4.oauthMetadataResponse)(request, metadataOptions);
|
|
2167
|
+
if (metadata !== void 0) {
|
|
2168
|
+
return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
|
|
2169
|
+
}
|
|
2170
|
+
if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
|
|
2171
|
+
if (request.method === "OPTIONS") {
|
|
2172
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2173
|
+
}
|
|
2174
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2175
|
+
return publicMetadataCors(Response.json({
|
|
2176
|
+
specVersion: "1.0",
|
|
2177
|
+
host: {
|
|
2178
|
+
displayName: "TinyCloud, Inc.",
|
|
2179
|
+
identifier: "https://mcp.tinycloud.xyz",
|
|
2180
|
+
documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
|
|
2181
|
+
},
|
|
2182
|
+
entries: [{
|
|
2183
|
+
identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
|
|
2184
|
+
displayName: "TinyCloud MCP",
|
|
2185
|
+
type: "application/json",
|
|
2186
|
+
description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
|
|
2187
|
+
url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
|
|
2188
|
+
representativeQueries: [
|
|
2189
|
+
"How do I connect to TinyCloud's hosted MCP server?",
|
|
2190
|
+
"Which endpoint provides TinyCloud's delegated MCP operations?",
|
|
2191
|
+
"How can I connect to TinyCloud MCP using local stdio?"
|
|
2192
|
+
]
|
|
2193
|
+
}]
|
|
2194
|
+
}, {
|
|
2195
|
+
headers: {
|
|
2196
|
+
"cache-control": "public, max-age=300",
|
|
2197
|
+
"content-type": "application/json; charset=utf-8"
|
|
2198
|
+
}
|
|
2199
|
+
}), request);
|
|
2200
|
+
}
|
|
2201
|
+
if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
|
|
2202
|
+
if (request.method === "OPTIONS") {
|
|
2203
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2204
|
+
}
|
|
2205
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2206
|
+
return publicMetadataCors(Response.json({
|
|
2207
|
+
protocol_version: "0.1",
|
|
2208
|
+
organization: {
|
|
2209
|
+
name: "TinyCloud, Inc.",
|
|
2210
|
+
domain: resourceUrl.hostname
|
|
2211
|
+
},
|
|
2212
|
+
web: {}
|
|
2213
|
+
}, {
|
|
2214
|
+
headers: {
|
|
2215
|
+
"cache-control": "public, max-age=300",
|
|
2216
|
+
"content-type": "application/json; charset=utf-8"
|
|
2217
|
+
}
|
|
2218
|
+
}), request);
|
|
2219
|
+
}
|
|
2220
|
+
if (url.pathname === "/auth.md" && surface === "hosted") {
|
|
2221
|
+
if (request.method === "OPTIONS") {
|
|
2222
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2223
|
+
}
|
|
2224
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2225
|
+
const issuerUrl = new URL(issuer);
|
|
2226
|
+
const authorizationServerMetadataUrl = new URL(
|
|
2227
|
+
`/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
|
|
2228
|
+
issuerUrl.origin
|
|
2229
|
+
);
|
|
2230
|
+
const registrationEndpoint = config.oauthMetadata.registration_endpoint;
|
|
2231
|
+
const registrationNote = typeof registrationEndpoint === "string" ? `OpenKey's OAuth \`registration_endpoint\` is ${registrationEndpoint}; it registers OAuth clients, not TinyCloud users or agents.` : "The authorization server does not advertise an OAuth client registration endpoint.";
|
|
2232
|
+
const authDoc = [
|
|
2233
|
+
"# TinyCloud MCP auth.md",
|
|
2234
|
+
"",
|
|
2235
|
+
`Protected resource: ${resourceUrl}.`,
|
|
2236
|
+
"",
|
|
2237
|
+
"Send an OAuth access token in the `Authorization: Bearer` header.",
|
|
2238
|
+
"",
|
|
2239
|
+
`Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
|
|
2240
|
+
`Authorization server issuer: ${issuer}.`,
|
|
2241
|
+
`Authorization server metadata: ${authorizationServerMetadataUrl}.`,
|
|
2242
|
+
`Scope: \`${REQUIRED_SCOPE}\`.`,
|
|
2243
|
+
"",
|
|
2244
|
+
"Hosted access uses OpenKey OAuth.",
|
|
2245
|
+
registrationNote,
|
|
2246
|
+
"TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
|
|
2247
|
+
"No `agent_auth` registration block is published because no agent-registration endpoint is available.",
|
|
2248
|
+
"",
|
|
2249
|
+
`Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
|
|
2250
|
+
"",
|
|
2251
|
+
"Connection steps: https://docs.tinycloud.xyz/cli/mcp"
|
|
2252
|
+
].join("\n") + "\n";
|
|
2253
|
+
return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
|
|
2254
|
+
}
|
|
2255
|
+
if (url.pathname === "/robots.txt" && surface === "hosted") {
|
|
2256
|
+
return request.method === "GET" ? publicText(`User-agent: *
|
|
2257
|
+
Allow: /
|
|
2258
|
+
Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
|
|
2259
|
+
Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
2260
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2261
|
+
}
|
|
2262
|
+
if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
|
|
2263
|
+
if (request.method === "OPTIONS") {
|
|
2264
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2265
|
+
}
|
|
2266
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2267
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2268
|
+
const card = {
|
|
2269
|
+
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
2270
|
+
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
2271
|
+
title: "TinyCloud MCP",
|
|
2272
|
+
version: MCP_VERSION,
|
|
2273
|
+
description: "MCP server for delegated TinyCloud operations.",
|
|
2274
|
+
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
2275
|
+
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
2276
|
+
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
2277
|
+
};
|
|
2278
|
+
if (identity !== void 0) {
|
|
2279
|
+
card["x-tinycloud-native-auth"] = {
|
|
2280
|
+
serviceDid: identity.serviceDid,
|
|
2281
|
+
serviceDidField: "x-tinycloud-native-auth.serviceDid",
|
|
2282
|
+
grantEndpoint: {
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
path: "/native/grant",
|
|
2285
|
+
body: { delegation: "serialized PortableDelegation JSON string" }
|
|
2286
|
+
},
|
|
2287
|
+
serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
|
|
2288
|
+
serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
|
|
2289
|
+
ownerDidHeader: NATIVE_OWNER_DID_HEADER,
|
|
2290
|
+
maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
|
|
2291
|
+
acceptedCapabilities: [
|
|
2292
|
+
{ service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
|
|
2293
|
+
{ service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
|
|
2294
|
+
],
|
|
2295
|
+
tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
|
|
2296
|
+
errors: {
|
|
2297
|
+
required: NATIVE_GRANT_REQUIRED,
|
|
2298
|
+
expired: NATIVE_GRANT_EXPIRED,
|
|
2299
|
+
rejected: NATIVE_GRANT_REJECTED,
|
|
2300
|
+
serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
|
|
2301
|
+
serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
|
|
2302
|
+
validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
return publicMetadataCors(Response.json(card, {
|
|
2307
|
+
headers: { "cache-control": "public, max-age=300" }
|
|
2308
|
+
}), request);
|
|
2309
|
+
}
|
|
2310
|
+
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
2311
|
+
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
2312
|
+
|
|
2313
|
+
Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
|
|
2314
|
+
|
|
2315
|
+
Connection documentation: https://docs.tinycloud.xyz/cli/mcp
|
|
2316
|
+
|
|
2317
|
+
Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
|
|
2318
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2319
|
+
}
|
|
2320
|
+
if (url.pathname === "/sitemap.xml" && surface === "hosted") {
|
|
2321
|
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${new URL("/", resourceUrl)}</loc></url></urlset>`;
|
|
2322
|
+
return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
|
|
2323
|
+
}
|
|
2324
|
+
if (url.pathname === "/" && surface === "hosted") {
|
|
2325
|
+
if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
|
|
2326
|
+
const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
|
|
2327
|
+
const content = markdown ? `# TinyCloud MCP
|
|
2328
|
+
|
|
2329
|
+
Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
|
|
2330
|
+
|
|
2331
|
+
[Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
|
|
2332
|
+
|
|
2333
|
+
Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
|
|
2334
|
+
` : `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="ai-catalog" href="/.well-known/ai-catalog.json"><title>TinyCloud MCP</title></head><main><h1>TinyCloud MCP</h1><p>Hosted MCP endpoint: <code>https://mcp.tinycloud.xyz/mcp</code> (Streamable HTTP, OpenKey OAuth).</p><p><a href="https://docs.tinycloud.xyz/cli/mcp">Connection documentation</a></p><p>Local alternative: install <code>${LOCAL_CLI_PACKAGE}</code> and <code>${LOCAL_MCP_PACKAGE}</code> and connect the <code>tinycloud-mcp</code> stdio server.</p></main></html>`;
|
|
2335
|
+
const headers = new Headers({
|
|
2336
|
+
"content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
|
|
2337
|
+
"cache-control": "public, max-age=300",
|
|
2338
|
+
"content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
2339
|
+
"x-content-type-options": "nosniff",
|
|
2340
|
+
vary: "Accept"
|
|
2341
|
+
});
|
|
2342
|
+
headers.set(
|
|
2343
|
+
"link",
|
|
2344
|
+
`<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
|
|
2345
|
+
);
|
|
2346
|
+
return new Response(request.method === "HEAD" ? null : content, { headers });
|
|
2347
|
+
}
|
|
1579
2348
|
if (url.pathname === "/healthz") {
|
|
1580
2349
|
return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
|
|
1581
2350
|
}
|
|
1582
|
-
const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
|
|
1583
|
-
if (hostFailure !== void 0) return hostFailure;
|
|
1584
2351
|
if (url.pathname === connectPath) {
|
|
1585
2352
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1586
2353
|
try {
|
|
@@ -1636,6 +2403,73 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1636
2403
|
);
|
|
1637
2404
|
}
|
|
1638
2405
|
}
|
|
2406
|
+
if (url.pathname === "/native/grant" && surface === "hosted") {
|
|
2407
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
2408
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2409
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2410
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2411
|
+
}
|
|
2412
|
+
if (!serviceCredentialMatches(
|
|
2413
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2414
|
+
config.nativeServiceCredential
|
|
2415
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2416
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
2417
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2418
|
+
}
|
|
2419
|
+
const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
|
|
2420
|
+
if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2421
|
+
let body;
|
|
2422
|
+
try {
|
|
2423
|
+
body = JSON.parse(bodyText);
|
|
2424
|
+
} catch {
|
|
2425
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2426
|
+
}
|
|
2427
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
|
|
2428
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2429
|
+
}
|
|
2430
|
+
const grantBody = body;
|
|
2431
|
+
if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
|
|
2432
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
const accepted = await store.acceptNativeGrant(grantBody.delegation);
|
|
2436
|
+
return json({
|
|
2437
|
+
ownerDid: accepted.ownerDid,
|
|
2438
|
+
serviceDid: accepted.serviceDid,
|
|
2439
|
+
expiresAt: accepted.expiresAt
|
|
2440
|
+
}, 201);
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
return nativeGrantErrorResponse(error);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
|
|
2446
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2447
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2448
|
+
}
|
|
2449
|
+
if (!serviceCredentialMatches(
|
|
2450
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2451
|
+
config.nativeServiceCredential
|
|
2452
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2453
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2454
|
+
const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
|
|
2455
|
+
if (ownerHeader === null || ownerHeader.length === 0) {
|
|
2456
|
+
return json({ error: NATIVE_GRANT_REQUIRED }, 401);
|
|
2457
|
+
}
|
|
2458
|
+
const grant = await store.nativeGrantForOwner(ownerHeader);
|
|
2459
|
+
if (grant.status !== "ok") {
|
|
2460
|
+
const status = grant.status === "unavailable" ? 503 : 401;
|
|
2461
|
+
const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
|
|
2462
|
+
return json({ error: code }, status);
|
|
2463
|
+
}
|
|
2464
|
+
return handler.fetch(request, {
|
|
2465
|
+
authInfo: {
|
|
2466
|
+
token: "native-service",
|
|
2467
|
+
clientId: "tinycloud-native",
|
|
2468
|
+
scopes: [],
|
|
2469
|
+
extra: { nativeOwnerDid: grant.ownerDid }
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
1639
2473
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
1640
2474
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
1641
2475
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -1747,9 +2581,77 @@ function callbackCors(response, openkeyHost) {
|
|
|
1747
2581
|
headers.set("vary", "Origin");
|
|
1748
2582
|
return new Response(response.body, { status: response.status, headers });
|
|
1749
2583
|
}
|
|
2584
|
+
async function readBoundedBodyText(request, maxBytes) {
|
|
2585
|
+
const contentLength = request.headers.get("content-length");
|
|
2586
|
+
if (contentLength !== null) {
|
|
2587
|
+
const parsedLength = Number(contentLength);
|
|
2588
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
|
|
2589
|
+
}
|
|
2590
|
+
if (request.body === null) return "";
|
|
2591
|
+
const reader = request.body.getReader();
|
|
2592
|
+
const chunks = [];
|
|
2593
|
+
let total = 0;
|
|
2594
|
+
try {
|
|
2595
|
+
while (true) {
|
|
2596
|
+
const { done, value } = await reader.read();
|
|
2597
|
+
if (done) break;
|
|
2598
|
+
total += value.byteLength;
|
|
2599
|
+
if (total > maxBytes) {
|
|
2600
|
+
await reader.cancel();
|
|
2601
|
+
return void 0;
|
|
2602
|
+
}
|
|
2603
|
+
chunks.push(value);
|
|
2604
|
+
}
|
|
2605
|
+
} finally {
|
|
2606
|
+
reader.releaseLock();
|
|
2607
|
+
}
|
|
2608
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2609
|
+
}
|
|
2610
|
+
function nativeGrantErrorResponse(error) {
|
|
2611
|
+
if (!(error instanceof NativeGrantValidationError)) {
|
|
2612
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2613
|
+
}
|
|
2614
|
+
const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
|
|
2615
|
+
return json({ error: error.code }, unavailable ? 503 : 401);
|
|
2616
|
+
}
|
|
1750
2617
|
function json(value, status = 200) {
|
|
1751
2618
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
1752
2619
|
}
|
|
2620
|
+
async function withBearerMethodsSupported(response) {
|
|
2621
|
+
if (response.status === 204 || response.body === null) return response;
|
|
2622
|
+
const metadata = await response.json();
|
|
2623
|
+
const headers = new Headers(response.headers);
|
|
2624
|
+
headers.delete("content-length");
|
|
2625
|
+
headers.delete("content-encoding");
|
|
2626
|
+
return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
|
|
2627
|
+
status: response.status,
|
|
2628
|
+
statusText: response.statusText,
|
|
2629
|
+
headers
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
function publicMetadataCors(response, request) {
|
|
2633
|
+
const headers = new Headers(response.headers);
|
|
2634
|
+
headers.set("access-control-allow-origin", "*");
|
|
2635
|
+
headers.set("access-control-allow-methods", "GET, OPTIONS");
|
|
2636
|
+
const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
|
|
2637
|
+
if (requestedHeaders !== null) {
|
|
2638
|
+
headers.set("access-control-allow-headers", requestedHeaders);
|
|
2639
|
+
}
|
|
2640
|
+
headers.set(
|
|
2641
|
+
"vary",
|
|
2642
|
+
requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
|
|
2643
|
+
);
|
|
2644
|
+
return new Response(response.body, { status: response.status, headers });
|
|
2645
|
+
}
|
|
2646
|
+
function publicText(text, contentType) {
|
|
2647
|
+
return new Response(text, {
|
|
2648
|
+
headers: {
|
|
2649
|
+
"content-type": contentType,
|
|
2650
|
+
"cache-control": "public, max-age=300",
|
|
2651
|
+
"x-content-type-options": "nosniff"
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
1753
2655
|
function methodNotAllowed(allow) {
|
|
1754
2656
|
return new Response("Method not allowed", { status: 405, headers: { allow } });
|
|
1755
2657
|
}
|
|
@@ -1779,7 +2681,14 @@ async function main() {
|
|
|
1779
2681
|
approvalTtlSeconds: positiveInteger(process.env.TC_MCP_APPROVAL_TTL_SECONDS, 300),
|
|
1780
2682
|
delegationExpiry: process.env.TC_MCP_DELEGATION_EXPIRY ?? "1h",
|
|
1781
2683
|
allowedOrigins: commaList(process.env.TC_MCP_ALLOWED_ORIGINS),
|
|
1782
|
-
|
|
2684
|
+
...process.env.TC_MCP_NATIVE_SERVICE_CREDENTIAL === void 0 ? {} : { nativeServiceCredential: process.env.TC_MCP_NATIVE_SERVICE_CREDENTIAL },
|
|
2685
|
+
oauthMetadata,
|
|
2686
|
+
introspection: {
|
|
2687
|
+
endpoint: requiredUrl("TC_MCP_INTROSPECTION_URL").toString(),
|
|
2688
|
+
clientId: requiredString("TC_MCP_INTROSPECTION_CLIENT_ID"),
|
|
2689
|
+
clientSecret: requiredString("TC_MCP_INTROSPECTION_CLIENT_SECRET"),
|
|
2690
|
+
authMethod: introspectionAuthMethod(process.env.TC_MCP_INTROSPECTION_AUTH_METHOD)
|
|
2691
|
+
}
|
|
1783
2692
|
});
|
|
1784
2693
|
const nodeHandler = (0, import_node.toNodeHandler)(app);
|
|
1785
2694
|
const server = (0, import_node_http.createServer)(nodeHandler);
|
|
@@ -1812,6 +2721,11 @@ function requiredAbsolutePath(name) {
|
|
|
1812
2721
|
if (!(0, import_node_path4.isAbsolute)(value)) throw new Error(`${name} must be an absolute path.`);
|
|
1813
2722
|
return (0, import_node_path4.resolve)(value);
|
|
1814
2723
|
}
|
|
2724
|
+
function introspectionAuthMethod(value) {
|
|
2725
|
+
if (value === void 0 || value === "basic") return "basic";
|
|
2726
|
+
if (value === "post") return "post";
|
|
2727
|
+
throw new Error("TC_MCP_INTROSPECTION_AUTH_METHOD must be basic or post.");
|
|
2728
|
+
}
|
|
1815
2729
|
function positiveInteger(value, fallback) {
|
|
1816
2730
|
if (value === void 0) return fallback;
|
|
1817
2731
|
const parsed = Number(value);
|