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