@tinycloud/mcp 0.8.3-beta.2 → 0.8.3-beta.20
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.cjs
CHANGED
|
@@ -41,7 +41,9 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
41
41
|
issuer: config.issuer,
|
|
42
42
|
audience: config.audience
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
const info = authInfo(token, payload, config);
|
|
45
|
+
if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
|
|
46
|
+
return info;
|
|
45
47
|
} catch (error) {
|
|
46
48
|
if (error instanceof import_jose.errors.JOSEError) {
|
|
47
49
|
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
@@ -51,6 +53,16 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
51
53
|
}
|
|
52
54
|
};
|
|
53
55
|
}
|
|
56
|
+
async function requireUsableStatus(checker, token) {
|
|
57
|
+
const status = await checker.tokenStatus(token);
|
|
58
|
+
if (status === "active") return;
|
|
59
|
+
if (status === "inactive") {
|
|
60
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
|
|
61
|
+
}
|
|
62
|
+
if (!checker.toleratesUnknown(token)) {
|
|
63
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
54
66
|
function authInfo(token, payload, config) {
|
|
55
67
|
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
56
68
|
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token does not identify a subject.");
|
|
@@ -104,6 +116,150 @@ function firstString(...values) {
|
|
|
104
116
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
// src/token-status.ts
|
|
120
|
+
var import_node_crypto = require("crypto");
|
|
121
|
+
var import_jose2 = require("jose");
|
|
122
|
+
var ACTIVE_CACHE_MS = 3e4;
|
|
123
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
124
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
125
|
+
var MAX_CACHED_TOKENS = 1e4;
|
|
126
|
+
function createTokenStatusChecker(config) {
|
|
127
|
+
const doFetch = config.fetch ?? fetch;
|
|
128
|
+
const clock = config.now ?? Date.now;
|
|
129
|
+
const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
130
|
+
`));
|
|
131
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
132
|
+
const inactive = /* @__PURE__ */ new Map();
|
|
133
|
+
const active = /* @__PURE__ */ new Map();
|
|
134
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
135
|
+
const confirmedActive = /* @__PURE__ */ new Map();
|
|
136
|
+
let credentialsRefused = false;
|
|
137
|
+
async function introspect(token) {
|
|
138
|
+
const headers = {
|
|
139
|
+
accept: "application/json",
|
|
140
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
141
|
+
};
|
|
142
|
+
const body = new URLSearchParams({ token, token_type_hint: "access_token" });
|
|
143
|
+
if (config.authMethod === "basic") {
|
|
144
|
+
const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
145
|
+
const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
|
|
146
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
147
|
+
} else {
|
|
148
|
+
body.set("client_id", config.clientId);
|
|
149
|
+
body.set("client_secret", config.clientSecret);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const response = await doFetch(config.endpoint, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers,
|
|
155
|
+
body,
|
|
156
|
+
redirect: "error",
|
|
157
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
158
|
+
});
|
|
159
|
+
if (response.status === 401) return "credentials";
|
|
160
|
+
if (response.status !== 200) return "unknown";
|
|
161
|
+
const result = await response.json();
|
|
162
|
+
if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
|
|
163
|
+
if (result.active === false) return "inactive";
|
|
164
|
+
if (result.active !== true) return "unknown";
|
|
165
|
+
return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
|
|
166
|
+
} catch {
|
|
167
|
+
return "unknown";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function query(key, token) {
|
|
171
|
+
const outcome = await introspect(token);
|
|
172
|
+
if (outcome === "credentials") {
|
|
173
|
+
if (!credentialsRefused) {
|
|
174
|
+
log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
|
|
175
|
+
}
|
|
176
|
+
credentialsRefused = true;
|
|
177
|
+
return "unknown";
|
|
178
|
+
}
|
|
179
|
+
if (outcome === "unknown") return outcome;
|
|
180
|
+
const now = clock();
|
|
181
|
+
prune(now);
|
|
182
|
+
if (outcome === "mismatch") {
|
|
183
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
184
|
+
confirmedActive.delete(key);
|
|
185
|
+
return "inactive";
|
|
186
|
+
}
|
|
187
|
+
credentialsRefused = false;
|
|
188
|
+
if (outcome === "inactive") {
|
|
189
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
190
|
+
confirmedActive.delete(key);
|
|
191
|
+
} else {
|
|
192
|
+
active.set(key, now + ACTIVE_CACHE_MS);
|
|
193
|
+
confirmedActive.delete(key);
|
|
194
|
+
confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
|
|
195
|
+
}
|
|
196
|
+
return outcome;
|
|
197
|
+
}
|
|
198
|
+
function prune(now) {
|
|
199
|
+
for (const [key, until] of active) if (until <= now) active.delete(key);
|
|
200
|
+
for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
|
|
201
|
+
for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
|
|
202
|
+
for (const cache of [active, inactive, confirmedActive]) {
|
|
203
|
+
while (cache.size >= MAX_CACHED_TOKENS) {
|
|
204
|
+
const oldest = cache.keys().next().value;
|
|
205
|
+
if (oldest === void 0) break;
|
|
206
|
+
cache.delete(oldest);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
async tokenStatus(token) {
|
|
212
|
+
const key = (0, import_node_crypto.createHash)("sha256").update(token).digest("hex");
|
|
213
|
+
const now = clock();
|
|
214
|
+
const inactiveUntil = inactive.get(key);
|
|
215
|
+
if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
|
|
216
|
+
const activeUntil = active.get(key);
|
|
217
|
+
if (activeUntil !== void 0 && activeUntil > now) return "active";
|
|
218
|
+
const pending = inFlight.get(key);
|
|
219
|
+
if (pending !== void 0) return pending;
|
|
220
|
+
const asking = query(key, token).finally(() => inFlight.delete(key));
|
|
221
|
+
inFlight.set(key, asking);
|
|
222
|
+
return asking;
|
|
223
|
+
},
|
|
224
|
+
toleratesUnknown(token) {
|
|
225
|
+
if (credentialsRefused) return false;
|
|
226
|
+
const confirmed = confirmedActive.get((0, import_node_crypto.createHash)("sha256").update(token).digest("hex"));
|
|
227
|
+
if (confirmed === void 0) return false;
|
|
228
|
+
const now = clock();
|
|
229
|
+
return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function answerMatchesToken(answer, token, resourceUrl) {
|
|
234
|
+
const jwt = claims(token);
|
|
235
|
+
if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
|
|
236
|
+
const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
|
|
237
|
+
if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
|
|
238
|
+
if (jwt.aud === void 0) return true;
|
|
239
|
+
const answered = audienceList(answer.aud);
|
|
240
|
+
const signed = audienceList(jwt.aud);
|
|
241
|
+
return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
|
|
242
|
+
}
|
|
243
|
+
function audienceList(value) {
|
|
244
|
+
const list = typeof value === "string" ? [value] : value;
|
|
245
|
+
return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
|
|
246
|
+
}
|
|
247
|
+
function tokenExpiryMs(token, now) {
|
|
248
|
+
const exp = claims(token).exp;
|
|
249
|
+
return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
|
|
250
|
+
}
|
|
251
|
+
function tokenLifetimeSeconds(token) {
|
|
252
|
+
const { exp, iat } = claims(token);
|
|
253
|
+
return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
254
|
+
}
|
|
255
|
+
function claims(token) {
|
|
256
|
+
try {
|
|
257
|
+
return (0, import_jose2.decodeJwt)(token);
|
|
258
|
+
} catch {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
107
263
|
// src/logging.ts
|
|
108
264
|
function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
109
265
|
process.stderr.write(`${JSON.stringify({
|
|
@@ -116,16 +272,85 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
|
116
272
|
}
|
|
117
273
|
|
|
118
274
|
// src/remote-store.ts
|
|
119
|
-
var
|
|
275
|
+
var import_node_crypto3 = require("crypto");
|
|
120
276
|
var import_promises = require("fs/promises");
|
|
121
277
|
var import_node_path = require("path");
|
|
122
278
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
123
279
|
var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
|
|
280
|
+
var import_artifacts = require("@tinycloud/operations/artifacts");
|
|
281
|
+
var import_state = require("@tinycloud/operations/state");
|
|
124
282
|
var import_operations = require("@tinycloud/operations");
|
|
125
283
|
var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
|
|
126
284
|
var import_tc_apps = require("@tinycloud/operations/tc-apps");
|
|
285
|
+
|
|
286
|
+
// src/native-auth.ts
|
|
287
|
+
var import_node_crypto2 = require("crypto");
|
|
288
|
+
var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
|
|
289
|
+
var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
|
|
290
|
+
var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
291
|
+
var NATIVE_GRANT_REQUIRED = "native_grant_required";
|
|
292
|
+
var NATIVE_GRANT_EXPIRED = "native_grant_expired";
|
|
293
|
+
var NATIVE_GRANT_REJECTED = "native_grant_rejected";
|
|
294
|
+
var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
|
|
295
|
+
var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
|
|
296
|
+
var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
|
|
297
|
+
var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
|
|
298
|
+
function serviceCredentialMatches(supplied, configured) {
|
|
299
|
+
if (supplied === null || configured === void 0) return false;
|
|
300
|
+
const suppliedBytes = Buffer.from(supplied, "utf8");
|
|
301
|
+
const configuredBytes = Buffer.from(configured, "utf8");
|
|
302
|
+
return suppliedBytes.byteLength === configuredBytes.byteLength && (0, import_node_crypto2.timingSafeEqual)(suppliedBytes, configuredBytes);
|
|
303
|
+
}
|
|
304
|
+
var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
|
|
305
|
+
var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
|
|
306
|
+
var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
|
|
307
|
+
function canonicalOwnerDid(value) {
|
|
308
|
+
const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
|
|
309
|
+
if (!match) return void 0;
|
|
310
|
+
const chainId = Number(match[1]);
|
|
311
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
312
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
313
|
+
}
|
|
314
|
+
function nativeOwnerForConnectorGrant(permissions) {
|
|
315
|
+
if (permissions.length !== 2) return void 0;
|
|
316
|
+
const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
|
|
317
|
+
const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
|
|
318
|
+
if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
|
|
319
|
+
const sql = sqlEntries[0];
|
|
320
|
+
const kv = kvEntries[0];
|
|
321
|
+
if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
|
|
322
|
+
if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
|
|
323
|
+
if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
|
|
324
|
+
const sqlSpace = ownerFromApplicationsSpace(sql.space);
|
|
325
|
+
const kvSpace = ownerFromApplicationsSpace(kv.space);
|
|
326
|
+
return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
|
|
327
|
+
}
|
|
328
|
+
function ownerFromApplicationsSpace(space) {
|
|
329
|
+
const match = OWNER_APPLICATIONS_SPACE.exec(space);
|
|
330
|
+
if (!match) return void 0;
|
|
331
|
+
const chainId = Number(match[1]);
|
|
332
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
333
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
334
|
+
}
|
|
335
|
+
function actionsEqual(actual, expected) {
|
|
336
|
+
const actualActions = [...actual].sort();
|
|
337
|
+
const expectedActions = [...expected].sort();
|
|
338
|
+
return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
|
|
339
|
+
}
|
|
340
|
+
function hasCaveats(permission) {
|
|
341
|
+
return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/remote-store.ts
|
|
127
345
|
var PROFILE = "agent";
|
|
128
346
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
347
|
+
var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
348
|
+
"DELEGATION_ARTIFACT_INVALID",
|
|
349
|
+
"DELEGATION_EXPIRED",
|
|
350
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
351
|
+
"DELEGATION_HOST_MISMATCH",
|
|
352
|
+
"DELEGATION_REJECTED"
|
|
353
|
+
]);
|
|
129
354
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
130
355
|
var BOOTSTRAP_PERMISSIONS = [
|
|
131
356
|
{
|
|
@@ -167,6 +392,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
167
392
|
this.detail = detail;
|
|
168
393
|
}
|
|
169
394
|
};
|
|
395
|
+
var NativeGrantValidationError = class extends Error {
|
|
396
|
+
code;
|
|
397
|
+
constructor(status) {
|
|
398
|
+
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";
|
|
399
|
+
super(code);
|
|
400
|
+
this.name = "NativeGrantValidationError";
|
|
401
|
+
this.code = code;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
170
404
|
var RemoteTenantStore = class {
|
|
171
405
|
#config;
|
|
172
406
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -179,9 +413,186 @@ var RemoteTenantStore = class {
|
|
|
179
413
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
180
414
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
181
415
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
416
|
+
#nativeIdentity;
|
|
417
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
182
418
|
constructor(config) {
|
|
183
419
|
this.#config = config;
|
|
184
420
|
}
|
|
421
|
+
async nativeServiceIdentity() {
|
|
422
|
+
const identity = await this.#loadNativeServiceKey();
|
|
423
|
+
return { serviceDid: normalizeDid(identity.did) };
|
|
424
|
+
}
|
|
425
|
+
nativeTenantStateRoot(ownerDid) {
|
|
426
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
427
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
428
|
+
const tenant = (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
|
|
429
|
+
return (0, import_node_path.join)(this.#config.stateDir, "native-tenants", tenant);
|
|
430
|
+
}
|
|
431
|
+
async acceptNativeGrant(serializedDelegation) {
|
|
432
|
+
if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
|
|
433
|
+
throw new NativeGrantValidationError("rejected");
|
|
434
|
+
}
|
|
435
|
+
const identity = await this.#loadNativeServiceKey();
|
|
436
|
+
const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
|
|
437
|
+
const { ownerDid, spaceId, permissions, expiresAt } = activated;
|
|
438
|
+
return this.#withNativeGrantLock(ownerDid, async () => {
|
|
439
|
+
const stateRoot = this.nativeTenantStateRoot(ownerDid);
|
|
440
|
+
let previous = null;
|
|
441
|
+
try {
|
|
442
|
+
const stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
443
|
+
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))) {
|
|
444
|
+
previous = stored;
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
448
|
+
}
|
|
449
|
+
if (previous !== null) {
|
|
450
|
+
const previousExpiry = Date.parse(previous.expiresAt);
|
|
451
|
+
const nextExpiry = expiresAt.getTime();
|
|
452
|
+
if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
|
|
453
|
+
throw new NativeGrantValidationError("rejected");
|
|
454
|
+
}
|
|
455
|
+
if (previous.delegationCid === activated.cid) {
|
|
456
|
+
return {
|
|
457
|
+
ownerDid,
|
|
458
|
+
serviceDid: normalizeDid(identity.did),
|
|
459
|
+
expiresAt: previous.expiresAt,
|
|
460
|
+
stateRoot
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const session = {
|
|
465
|
+
delegationHeader: activated.delegation.delegationHeader,
|
|
466
|
+
delegationCid: activated.cid,
|
|
467
|
+
spaceId,
|
|
468
|
+
jwk: identity.jwk,
|
|
469
|
+
verificationMethod: identity.did,
|
|
470
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
471
|
+
};
|
|
472
|
+
const grant = {
|
|
473
|
+
version: 1,
|
|
474
|
+
ownerDid,
|
|
475
|
+
serviceDid: normalizeDid(identity.did),
|
|
476
|
+
serializedDelegation: (0, import_node_sdk2.serializeDelegation)(activated.delegation),
|
|
477
|
+
delegationCid: activated.cid,
|
|
478
|
+
spaceId,
|
|
479
|
+
permissions,
|
|
480
|
+
expiresAt: expiresAt.toISOString()
|
|
481
|
+
};
|
|
482
|
+
const sessionPath = this.#sessionPath(stateRoot);
|
|
483
|
+
const previousSession = await readOptionalText(sessionPath);
|
|
484
|
+
try {
|
|
485
|
+
await secureWrite(this.#profilePath(stateRoot), {
|
|
486
|
+
name: PROFILE,
|
|
487
|
+
host: this.#config.nodeHost,
|
|
488
|
+
chainId: activated.delegation.chainId,
|
|
489
|
+
spaceName: "applications",
|
|
490
|
+
did: identity.did,
|
|
491
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
492
|
+
profile: PROFILE,
|
|
493
|
+
operatorType: "agent",
|
|
494
|
+
posture: "delegate-session",
|
|
495
|
+
ownerDid,
|
|
496
|
+
sessionDid: identity.did,
|
|
497
|
+
spaceId,
|
|
498
|
+
tinycloudHost: this.#config.nodeHost
|
|
499
|
+
});
|
|
500
|
+
await secureWrite(this.#keyPath(stateRoot), identity.jwk);
|
|
501
|
+
await secureWrite(sessionPath, session);
|
|
502
|
+
const request = await (0, import_state.withTinyCloudStateRoot)(stateRoot, () => (0, import_artifacts.createOrReusePermissionRequest)({
|
|
503
|
+
profile: PROFILE,
|
|
504
|
+
posture: "delegate-session",
|
|
505
|
+
operatorType: "agent",
|
|
506
|
+
host: this.#config.nodeHost,
|
|
507
|
+
sessionDid: identity.did,
|
|
508
|
+
ownerDid,
|
|
509
|
+
spaceId,
|
|
510
|
+
missing: permissions,
|
|
511
|
+
granted: [],
|
|
512
|
+
replace: true
|
|
513
|
+
}));
|
|
514
|
+
if (request.status !== "created") {
|
|
515
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
516
|
+
}
|
|
517
|
+
const imported = await (0, import_operations.invokeOperation)(
|
|
518
|
+
"tinycloud.auth.import",
|
|
519
|
+
1,
|
|
520
|
+
{ profile: PROFILE, stateRoot },
|
|
521
|
+
{
|
|
522
|
+
kind: "tinycloud.auth.delegation",
|
|
523
|
+
version: 1,
|
|
524
|
+
requestId: request.request.requestId,
|
|
525
|
+
delegationCid: activated.cid,
|
|
526
|
+
delegation: activated.delegation
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
if (imported.status === "error") {
|
|
530
|
+
throw new NativeGrantValidationError(
|
|
531
|
+
NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
|
|
535
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
536
|
+
}
|
|
537
|
+
await secureWrite(this.#nativeGrantPath(stateRoot), grant);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
try {
|
|
540
|
+
await restoreOptionalText(sessionPath, previousSession);
|
|
541
|
+
} catch {
|
|
542
|
+
await (0, import_promises.rm)(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
|
|
543
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
544
|
+
}
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async nativeGrantForOwner(ownerDid) {
|
|
551
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
552
|
+
if (canonical === void 0) return { status: "rejected" };
|
|
553
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
554
|
+
let stored;
|
|
555
|
+
try {
|
|
556
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
557
|
+
} catch {
|
|
558
|
+
return { status: "rejected" };
|
|
559
|
+
}
|
|
560
|
+
if (stored === null) return { status: "required" };
|
|
561
|
+
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) {
|
|
562
|
+
return { status: "rejected" };
|
|
563
|
+
}
|
|
564
|
+
const expires = Date.parse(stored.expiresAt);
|
|
565
|
+
if (!Number.isFinite(expires)) return { status: "rejected" };
|
|
566
|
+
if (expires <= Date.now()) return { status: "expired" };
|
|
567
|
+
try {
|
|
568
|
+
const identity = await this.#loadNativeServiceKey();
|
|
569
|
+
if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
|
|
570
|
+
const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
|
|
571
|
+
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" };
|
|
572
|
+
return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (error instanceof NativeGrantValidationError) {
|
|
575
|
+
return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
|
|
576
|
+
}
|
|
577
|
+
return { status: "unavailable" };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async assertNativeGrantActive(ownerDid) {
|
|
581
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
582
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
583
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
584
|
+
let stored;
|
|
585
|
+
try {
|
|
586
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
587
|
+
} catch {
|
|
588
|
+
throw new NativeGrantValidationError("rejected");
|
|
589
|
+
}
|
|
590
|
+
if (stored === null) throw new NativeGrantValidationError("required");
|
|
591
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
592
|
+
throw new NativeGrantValidationError("rejected");
|
|
593
|
+
}
|
|
594
|
+
if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
|
|
595
|
+
}
|
|
185
596
|
tenantStateRoot(subject) {
|
|
186
597
|
return (0, import_node_path.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
187
598
|
}
|
|
@@ -708,7 +1119,7 @@ var RemoteTenantStore = class {
|
|
|
708
1119
|
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
709
1120
|
);
|
|
710
1121
|
if (reusable !== void 0) return reusable;
|
|
711
|
-
const nonce = (0,
|
|
1122
|
+
const nonce = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
|
|
712
1123
|
const created = Date.now();
|
|
713
1124
|
const pending = {
|
|
714
1125
|
version: 1,
|
|
@@ -733,7 +1144,7 @@ var RemoteTenantStore = class {
|
|
|
733
1144
|
const expected = this.#signState(tenantId, nonce);
|
|
734
1145
|
const supplied = Buffer.from(signature, "base64url");
|
|
735
1146
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
736
|
-
if (supplied.length !== expectedBytes.length || !(0,
|
|
1147
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto3.timingSafeEqual)(supplied, expectedBytes)) {
|
|
737
1148
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
738
1149
|
}
|
|
739
1150
|
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -784,11 +1195,128 @@ var RemoteTenantStore = class {
|
|
|
784
1195
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
785
1196
|
return url.toString();
|
|
786
1197
|
}
|
|
1198
|
+
async #withNativeGrantLock(ownerDid, action) {
|
|
1199
|
+
const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
|
|
1200
|
+
let releaseNext;
|
|
1201
|
+
const next = new Promise((resolve) => {
|
|
1202
|
+
releaseNext = resolve;
|
|
1203
|
+
});
|
|
1204
|
+
this.#nativeGrantQueues.set(ownerDid, next);
|
|
1205
|
+
await previous;
|
|
1206
|
+
try {
|
|
1207
|
+
return await action();
|
|
1208
|
+
} finally {
|
|
1209
|
+
releaseNext();
|
|
1210
|
+
if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
async #loadNativeServiceKey() {
|
|
1214
|
+
if (this.#nativeIdentity === void 0) {
|
|
1215
|
+
const loading = this.#readOrCreateNativeServiceKey();
|
|
1216
|
+
this.#nativeIdentity = loading;
|
|
1217
|
+
void loading.catch(() => {
|
|
1218
|
+
if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
return this.#nativeIdentity;
|
|
1222
|
+
}
|
|
1223
|
+
async #readOrCreateNativeServiceKey() {
|
|
1224
|
+
const path = (0, import_node_path.join)(this.#config.stateDir, ".tinycloud", "native-service-key.json");
|
|
1225
|
+
await secureDirectory((0, import_node_path.dirname)(path));
|
|
1226
|
+
let stored = await readJson(path);
|
|
1227
|
+
if (stored === null) {
|
|
1228
|
+
const generated = generateSessionKey();
|
|
1229
|
+
try {
|
|
1230
|
+
await (0, import_promises.writeFile)(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
|
|
1231
|
+
`, {
|
|
1232
|
+
mode: 384,
|
|
1233
|
+
flag: "wx"
|
|
1234
|
+
});
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
if (!isRecord(error) || error.code !== "EEXIST") throw error;
|
|
1237
|
+
}
|
|
1238
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1239
|
+
stored = await readJson(path);
|
|
1240
|
+
}
|
|
1241
|
+
if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
|
|
1242
|
+
throw new Error("The native MCP service identity is unavailable.");
|
|
1243
|
+
}
|
|
1244
|
+
return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
|
|
1245
|
+
}
|
|
1246
|
+
async #activateNativeDelegation(serialized, identity) {
|
|
1247
|
+
let delegation;
|
|
1248
|
+
try {
|
|
1249
|
+
const parsed = JSON.parse(serialized);
|
|
1250
|
+
if (!isRecord(parsed)) throw new Error("invalid delegation");
|
|
1251
|
+
delegation = (0, import_node_sdk2.deserializeDelegation)(serialized);
|
|
1252
|
+
} catch {
|
|
1253
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1254
|
+
}
|
|
1255
|
+
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())) {
|
|
1256
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1257
|
+
}
|
|
1258
|
+
const now = Date.now();
|
|
1259
|
+
const claimedExpiry = delegation.expiry.getTime();
|
|
1260
|
+
if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
|
|
1261
|
+
if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1262
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1263
|
+
}
|
|
1264
|
+
let ownerDid;
|
|
1265
|
+
let spaceId;
|
|
1266
|
+
const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
|
|
1267
|
+
try {
|
|
1268
|
+
await node.restoreSession({
|
|
1269
|
+
delegationHeader: delegation.delegationHeader,
|
|
1270
|
+
delegationCid: delegation.cid,
|
|
1271
|
+
spaceId: delegation.spaceId,
|
|
1272
|
+
jwk: identity.jwk,
|
|
1273
|
+
verificationMethod: identity.did,
|
|
1274
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1275
|
+
});
|
|
1276
|
+
const activated = await (0, import_node_sdk2.activateValidatedRuntimeDelegation)(node, delegation, {
|
|
1277
|
+
host: this.#config.nodeHost,
|
|
1278
|
+
authorize: (permissions) => {
|
|
1279
|
+
const derivedOwner = nativeOwnerForConnectorGrant(permissions);
|
|
1280
|
+
const signedSpace = permissions[0]?.space;
|
|
1281
|
+
if (derivedOwner === void 0 || signedSpace === void 0) return false;
|
|
1282
|
+
const metadataOwner = canonicalOwnerDid(
|
|
1283
|
+
`did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
|
|
1284
|
+
);
|
|
1285
|
+
if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
|
|
1286
|
+
ownerDid = derivedOwner;
|
|
1287
|
+
spaceId = signedSpace;
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1292
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
ownerDid,
|
|
1296
|
+
spaceId,
|
|
1297
|
+
permissions: [...activated.effectivePermissions],
|
|
1298
|
+
cid: activated.cid,
|
|
1299
|
+
expiresAt: activated.expiry,
|
|
1300
|
+
delegation: activated.delegation
|
|
1301
|
+
};
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
if (error instanceof NativeGrantValidationError) throw error;
|
|
1304
|
+
if (error instanceof import_node_sdk2.RuntimeDelegationRejectedError) {
|
|
1305
|
+
throw new NativeGrantValidationError(
|
|
1306
|
+
error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
#nativeGrantPath(stateRoot) {
|
|
1313
|
+
return (0, import_node_path.join)(stateRoot, ".tinycloud", "native-grant.json");
|
|
1314
|
+
}
|
|
787
1315
|
#tenantId(subject) {
|
|
788
|
-
return (0,
|
|
1316
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
789
1317
|
}
|
|
790
1318
|
#signState(tenantId, nonce) {
|
|
791
|
-
return (0,
|
|
1319
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
792
1320
|
}
|
|
793
1321
|
#profilePath(stateRoot) {
|
|
794
1322
|
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
@@ -877,6 +1405,21 @@ function samePrincipal(left, right) {
|
|
|
877
1405
|
function normalizeDid(value) {
|
|
878
1406
|
return value.split("#", 1)[0];
|
|
879
1407
|
}
|
|
1408
|
+
function sameNodeHost(left, right) {
|
|
1409
|
+
try {
|
|
1410
|
+
const normalize = (value) => {
|
|
1411
|
+
const url = new URL(value);
|
|
1412
|
+
if (url.username !== "" || url.password !== "") return "";
|
|
1413
|
+
url.hash = "";
|
|
1414
|
+
url.search = "";
|
|
1415
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
1416
|
+
return url.toString().replace(/\/+$/, "");
|
|
1417
|
+
};
|
|
1418
|
+
return normalize(left) !== "" && normalize(left) === normalize(right);
|
|
1419
|
+
} catch {
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
880
1423
|
function isPermissionEntry(value) {
|
|
881
1424
|
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");
|
|
882
1425
|
}
|
|
@@ -890,7 +1433,7 @@ async function secureDirectory(path) {
|
|
|
890
1433
|
async function secureWrite(path, value) {
|
|
891
1434
|
const directory = (0, import_node_path.dirname)(path);
|
|
892
1435
|
await secureDirectory(directory);
|
|
893
|
-
const temporary = `${path}.${process.pid}.${(0,
|
|
1436
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`;
|
|
894
1437
|
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
895
1438
|
`, { mode: 384 });
|
|
896
1439
|
await (0, import_promises.rename)(temporary, path);
|
|
@@ -907,6 +1450,23 @@ async function readJson(path) {
|
|
|
907
1450
|
throw error;
|
|
908
1451
|
}
|
|
909
1452
|
}
|
|
1453
|
+
async function readOptionalText(path) {
|
|
1454
|
+
try {
|
|
1455
|
+
return await (0, import_promises.readFile)(path, "utf8");
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
1458
|
+
throw error;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async function restoreOptionalText(path, contents) {
|
|
1462
|
+
if (contents === null) {
|
|
1463
|
+
await (0, import_promises.rm)(path, { force: true });
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
await secureDirectory((0, import_node_path.dirname)(path));
|
|
1467
|
+
await (0, import_promises.writeFile)(path, contents, { mode: 384 });
|
|
1468
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1469
|
+
}
|
|
910
1470
|
async function reusablePending(directory, expected) {
|
|
911
1471
|
let names;
|
|
912
1472
|
try {
|
|
@@ -1304,7 +1864,7 @@ function createJsonSchemaValidator() {
|
|
|
1304
1864
|
return new import_ajv.AjvJsonSchemaValidator();
|
|
1305
1865
|
}
|
|
1306
1866
|
function registerTinyCloudTools(server, selection, validator) {
|
|
1307
|
-
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
|
|
1867
|
+
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
|
|
1308
1868
|
for (const binding of bindings) {
|
|
1309
1869
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
1310
1870
|
if (operation === void 0) {
|
|
@@ -1382,7 +1942,7 @@ function outcomeCode(result) {
|
|
|
1382
1942
|
// package.json
|
|
1383
1943
|
var package_default = {
|
|
1384
1944
|
name: "@tinycloud/mcp",
|
|
1385
|
-
version: "0.8.3-beta.
|
|
1945
|
+
version: "0.8.3-beta.20",
|
|
1386
1946
|
description: "TinyCloud delegated operations MCP server",
|
|
1387
1947
|
keywords: [
|
|
1388
1948
|
"tinycloud",
|
|
@@ -1453,9 +2013,9 @@ var package_default = {
|
|
|
1453
2013
|
dependencies: {
|
|
1454
2014
|
"@modelcontextprotocol/node": "2.0.0-beta.5",
|
|
1455
2015
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
1456
|
-
"@tinycloud/node-sdk": "3.8.0-beta.
|
|
2016
|
+
"@tinycloud/node-sdk": "3.8.0-beta.18",
|
|
1457
2017
|
"@tinycloud/node-sdk-wasm": "1.7.8",
|
|
1458
|
-
"@tinycloud/operations": "0.7.7-beta.
|
|
2018
|
+
"@tinycloud/operations": "0.7.7-beta.17",
|
|
1459
2019
|
jose: "6.2.4"
|
|
1460
2020
|
},
|
|
1461
2021
|
devDependencies: {
|
|
@@ -1479,15 +2039,22 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
|
|
|
1479
2039
|
|
|
1480
2040
|
// src/http.ts
|
|
1481
2041
|
var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
2042
|
+
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
2043
|
+
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1482
2044
|
function createHostedMcpApp(config) {
|
|
2045
|
+
if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
|
|
2046
|
+
throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
|
|
2047
|
+
}
|
|
1483
2048
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1484
2049
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1485
|
-
const
|
|
2050
|
+
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
2051
|
+
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1486
2052
|
const plugin = createHostedSurface({
|
|
1487
2053
|
...config,
|
|
2054
|
+
nativeServiceCredential: void 0,
|
|
1488
2055
|
stateDir: (0, import_node_path3.join)(config.stateDir, "chatgpt"),
|
|
1489
2056
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1490
|
-
}, pluginUrl, "public");
|
|
2057
|
+
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
1491
2058
|
const pluginPaths = {
|
|
1492
2059
|
[pluginUrl.pathname]: true,
|
|
1493
2060
|
[new URL("./connect", pluginUrl).pathname]: true,
|
|
@@ -1501,7 +2068,7 @@ function createHostedMcpApp(config) {
|
|
|
1501
2068
|
}
|
|
1502
2069
|
};
|
|
1503
2070
|
}
|
|
1504
|
-
function createHostedSurface(config, resourceUrl, surface) {
|
|
2071
|
+
function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
1505
2072
|
const connectPath = config.approvalPath ?? "/connect";
|
|
1506
2073
|
const callbackPath = `${connectPath}/callback`;
|
|
1507
2074
|
const issuer = String(config.oauthMetadata.issuer);
|
|
@@ -1510,7 +2077,8 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1510
2077
|
issuer,
|
|
1511
2078
|
audience: resourceUrl.toString(),
|
|
1512
2079
|
jwksUri,
|
|
1513
|
-
requiredScope: REQUIRED_SCOPE
|
|
2080
|
+
requiredScope: REQUIRED_SCOPE,
|
|
2081
|
+
...tokenStatus === void 0 ? {} : { tokenStatus }
|
|
1514
2082
|
});
|
|
1515
2083
|
const resourceMetadataUrl = (0, import_server4.getOAuthProtectedResourceMetadataUrl)(resourceUrl);
|
|
1516
2084
|
const authenticate = (0, import_server4.requireBearerAuth)({
|
|
@@ -1519,16 +2087,19 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1519
2087
|
resourceMetadataUrl
|
|
1520
2088
|
});
|
|
1521
2089
|
const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
|
|
2090
|
+
const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
|
|
2091
|
+
if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
|
|
1522
2092
|
const handler = (0, import_server4.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
|
|
1523
|
-
const
|
|
2093
|
+
const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
|
|
2094
|
+
const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
|
|
1524
2095
|
const { subject, ownerDids } = identity;
|
|
1525
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2096
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1526
2097
|
const validator = createJsonSchemaValidator();
|
|
1527
2098
|
const server = new import_server4.McpServer(
|
|
1528
2099
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1529
2100
|
{
|
|
1530
2101
|
jsonSchemaValidator: validator,
|
|
1531
|
-
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."
|
|
2102
|
+
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."
|
|
1532
2103
|
}
|
|
1533
2104
|
);
|
|
1534
2105
|
registerTinyCloudTools(server, {
|
|
@@ -1537,10 +2108,15 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1537
2108
|
explicitProfile: true,
|
|
1538
2109
|
allowOwnerProfile: false,
|
|
1539
2110
|
stateRoot,
|
|
1540
|
-
|
|
1541
|
-
|
|
2111
|
+
...nativeOwner === void 0 ? {} : {
|
|
2112
|
+
allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
|
|
2113
|
+
},
|
|
2114
|
+
beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
|
|
2115
|
+
...nativeOwner === void 0 ? {
|
|
2116
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
2117
|
+
} : {}
|
|
1542
2118
|
}, validator);
|
|
1543
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2119
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1544
2120
|
return server;
|
|
1545
2121
|
}, {
|
|
1546
2122
|
legacy: "stateless",
|
|
@@ -1556,14 +2132,205 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1556
2132
|
const allowedHosts = [resourceUrl.hostname];
|
|
1557
2133
|
return {
|
|
1558
2134
|
async fetch(request) {
|
|
1559
|
-
const metadata = (0, import_server4.oauthMetadataResponse)(request, metadataOptions);
|
|
1560
|
-
if (metadata !== void 0) return metadata;
|
|
1561
2135
|
const url = new URL(request.url);
|
|
2136
|
+
if (url.pathname !== "/healthz") {
|
|
2137
|
+
const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
|
|
2138
|
+
if (hostFailure !== void 0) return hostFailure;
|
|
2139
|
+
}
|
|
2140
|
+
if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
|
|
2141
|
+
const metadataUrl = new URL((0, import_server4.getOAuthProtectedResourceMetadataUrl)(resourceUrl));
|
|
2142
|
+
const metadataRequest = new Request(metadataUrl.toString(), {
|
|
2143
|
+
method: request.method,
|
|
2144
|
+
headers: request.headers
|
|
2145
|
+
});
|
|
2146
|
+
const response = (0, import_server4.oauthMetadataResponse)(metadataRequest, metadataOptions);
|
|
2147
|
+
if (response !== void 0) return withBearerMethodsSupported(response);
|
|
2148
|
+
}
|
|
2149
|
+
const metadata = (0, import_server4.oauthMetadataResponse)(request, metadataOptions);
|
|
2150
|
+
if (metadata !== void 0) {
|
|
2151
|
+
return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
|
|
2152
|
+
}
|
|
2153
|
+
if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
|
|
2154
|
+
if (request.method === "OPTIONS") {
|
|
2155
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2156
|
+
}
|
|
2157
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2158
|
+
return publicMetadataCors(Response.json({
|
|
2159
|
+
specVersion: "1.0",
|
|
2160
|
+
host: {
|
|
2161
|
+
displayName: "TinyCloud, Inc.",
|
|
2162
|
+
identifier: "https://mcp.tinycloud.xyz",
|
|
2163
|
+
documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
|
|
2164
|
+
},
|
|
2165
|
+
entries: [{
|
|
2166
|
+
identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
|
|
2167
|
+
displayName: "TinyCloud MCP",
|
|
2168
|
+
type: "application/json",
|
|
2169
|
+
description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
|
|
2170
|
+
url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
|
|
2171
|
+
representativeQueries: [
|
|
2172
|
+
"How do I connect to TinyCloud's hosted MCP server?",
|
|
2173
|
+
"Which endpoint provides TinyCloud's delegated MCP operations?",
|
|
2174
|
+
"How can I connect to TinyCloud MCP using local stdio?"
|
|
2175
|
+
]
|
|
2176
|
+
}]
|
|
2177
|
+
}, {
|
|
2178
|
+
headers: {
|
|
2179
|
+
"cache-control": "public, max-age=300",
|
|
2180
|
+
"content-type": "application/json; charset=utf-8"
|
|
2181
|
+
}
|
|
2182
|
+
}), request);
|
|
2183
|
+
}
|
|
2184
|
+
if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
|
|
2185
|
+
if (request.method === "OPTIONS") {
|
|
2186
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2187
|
+
}
|
|
2188
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2189
|
+
return publicMetadataCors(Response.json({
|
|
2190
|
+
protocol_version: "0.1",
|
|
2191
|
+
organization: {
|
|
2192
|
+
name: "TinyCloud, Inc.",
|
|
2193
|
+
domain: resourceUrl.hostname
|
|
2194
|
+
},
|
|
2195
|
+
web: {}
|
|
2196
|
+
}, {
|
|
2197
|
+
headers: {
|
|
2198
|
+
"cache-control": "public, max-age=300",
|
|
2199
|
+
"content-type": "application/json; charset=utf-8"
|
|
2200
|
+
}
|
|
2201
|
+
}), request);
|
|
2202
|
+
}
|
|
2203
|
+
if (url.pathname === "/auth.md" && surface === "hosted") {
|
|
2204
|
+
if (request.method === "OPTIONS") {
|
|
2205
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2206
|
+
}
|
|
2207
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2208
|
+
const issuerUrl = new URL(issuer);
|
|
2209
|
+
const authorizationServerMetadataUrl = new URL(
|
|
2210
|
+
`/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
|
|
2211
|
+
issuerUrl.origin
|
|
2212
|
+
);
|
|
2213
|
+
const registrationEndpoint = config.oauthMetadata.registration_endpoint;
|
|
2214
|
+
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.";
|
|
2215
|
+
const authDoc = [
|
|
2216
|
+
"# TinyCloud MCP auth.md",
|
|
2217
|
+
"",
|
|
2218
|
+
`Protected resource: ${resourceUrl}.`,
|
|
2219
|
+
"",
|
|
2220
|
+
"Send an OAuth access token in the `Authorization: Bearer` header.",
|
|
2221
|
+
"",
|
|
2222
|
+
`Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
|
|
2223
|
+
`Authorization server issuer: ${issuer}.`,
|
|
2224
|
+
`Authorization server metadata: ${authorizationServerMetadataUrl}.`,
|
|
2225
|
+
`Scope: \`${REQUIRED_SCOPE}\`.`,
|
|
2226
|
+
"",
|
|
2227
|
+
"Hosted access uses OpenKey OAuth.",
|
|
2228
|
+
registrationNote,
|
|
2229
|
+
"TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
|
|
2230
|
+
"No `agent_auth` registration block is published because no agent-registration endpoint is available.",
|
|
2231
|
+
"",
|
|
2232
|
+
`Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
|
|
2233
|
+
"",
|
|
2234
|
+
"Connection steps: https://docs.tinycloud.xyz/cli/mcp"
|
|
2235
|
+
].join("\n") + "\n";
|
|
2236
|
+
return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
|
|
2237
|
+
}
|
|
2238
|
+
if (url.pathname === "/robots.txt" && surface === "hosted") {
|
|
2239
|
+
return request.method === "GET" ? publicText(`User-agent: *
|
|
2240
|
+
Allow: /
|
|
2241
|
+
Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
|
|
2242
|
+
Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
2243
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2244
|
+
}
|
|
2245
|
+
if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
|
|
2246
|
+
if (request.method === "OPTIONS") {
|
|
2247
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2248
|
+
}
|
|
2249
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2250
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2251
|
+
const card = {
|
|
2252
|
+
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
2253
|
+
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
2254
|
+
title: "TinyCloud MCP",
|
|
2255
|
+
version: MCP_VERSION,
|
|
2256
|
+
description: "MCP server for delegated TinyCloud operations.",
|
|
2257
|
+
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
2258
|
+
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
2259
|
+
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
2260
|
+
};
|
|
2261
|
+
if (identity !== void 0) {
|
|
2262
|
+
card["x-tinycloud-native-auth"] = {
|
|
2263
|
+
serviceDid: identity.serviceDid,
|
|
2264
|
+
serviceDidField: "x-tinycloud-native-auth.serviceDid",
|
|
2265
|
+
grantEndpoint: {
|
|
2266
|
+
method: "POST",
|
|
2267
|
+
path: "/native/grant",
|
|
2268
|
+
body: { delegation: "serialized PortableDelegation JSON string" }
|
|
2269
|
+
},
|
|
2270
|
+
serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
|
|
2271
|
+
serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
|
|
2272
|
+
ownerDidHeader: NATIVE_OWNER_DID_HEADER,
|
|
2273
|
+
maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
|
|
2274
|
+
acceptedCapabilities: [
|
|
2275
|
+
{ service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
|
|
2276
|
+
{ service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
|
|
2277
|
+
],
|
|
2278
|
+
tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
|
|
2279
|
+
errors: {
|
|
2280
|
+
required: NATIVE_GRANT_REQUIRED,
|
|
2281
|
+
expired: NATIVE_GRANT_EXPIRED,
|
|
2282
|
+
rejected: NATIVE_GRANT_REJECTED,
|
|
2283
|
+
serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
|
|
2284
|
+
serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
|
|
2285
|
+
validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
return publicMetadataCors(Response.json(card, {
|
|
2290
|
+
headers: { "cache-control": "public, max-age=300" }
|
|
2291
|
+
}), request);
|
|
2292
|
+
}
|
|
2293
|
+
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
2294
|
+
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
2295
|
+
|
|
2296
|
+
Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
|
|
2297
|
+
|
|
2298
|
+
Connection documentation: https://docs.tinycloud.xyz/cli/mcp
|
|
2299
|
+
|
|
2300
|
+
Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
|
|
2301
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2302
|
+
}
|
|
2303
|
+
if (url.pathname === "/sitemap.xml" && surface === "hosted") {
|
|
2304
|
+
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>`;
|
|
2305
|
+
return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
|
|
2306
|
+
}
|
|
2307
|
+
if (url.pathname === "/" && surface === "hosted") {
|
|
2308
|
+
if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
|
|
2309
|
+
const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
|
|
2310
|
+
const content = markdown ? `# TinyCloud MCP
|
|
2311
|
+
|
|
2312
|
+
Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
|
|
2313
|
+
|
|
2314
|
+
[Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
|
|
2315
|
+
|
|
2316
|
+
Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
|
|
2317
|
+
` : `<!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>`;
|
|
2318
|
+
const headers = new Headers({
|
|
2319
|
+
"content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
|
|
2320
|
+
"cache-control": "public, max-age=300",
|
|
2321
|
+
"content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
2322
|
+
"x-content-type-options": "nosniff",
|
|
2323
|
+
vary: "Accept"
|
|
2324
|
+
});
|
|
2325
|
+
headers.set(
|
|
2326
|
+
"link",
|
|
2327
|
+
`<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
|
|
2328
|
+
);
|
|
2329
|
+
return new Response(request.method === "HEAD" ? null : content, { headers });
|
|
2330
|
+
}
|
|
1562
2331
|
if (url.pathname === "/healthz") {
|
|
1563
2332
|
return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
|
|
1564
2333
|
}
|
|
1565
|
-
const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
|
|
1566
|
-
if (hostFailure !== void 0) return hostFailure;
|
|
1567
2334
|
if (url.pathname === connectPath) {
|
|
1568
2335
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1569
2336
|
try {
|
|
@@ -1619,6 +2386,73 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1619
2386
|
);
|
|
1620
2387
|
}
|
|
1621
2388
|
}
|
|
2389
|
+
if (url.pathname === "/native/grant" && surface === "hosted") {
|
|
2390
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
2391
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2392
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2393
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2394
|
+
}
|
|
2395
|
+
if (!serviceCredentialMatches(
|
|
2396
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2397
|
+
config.nativeServiceCredential
|
|
2398
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2399
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
2400
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2401
|
+
}
|
|
2402
|
+
const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
|
|
2403
|
+
if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2404
|
+
let body;
|
|
2405
|
+
try {
|
|
2406
|
+
body = JSON.parse(bodyText);
|
|
2407
|
+
} catch {
|
|
2408
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2409
|
+
}
|
|
2410
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
|
|
2411
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2412
|
+
}
|
|
2413
|
+
const grantBody = body;
|
|
2414
|
+
if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
|
|
2415
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2416
|
+
}
|
|
2417
|
+
try {
|
|
2418
|
+
const accepted = await store.acceptNativeGrant(grantBody.delegation);
|
|
2419
|
+
return json({
|
|
2420
|
+
ownerDid: accepted.ownerDid,
|
|
2421
|
+
serviceDid: accepted.serviceDid,
|
|
2422
|
+
expiresAt: accepted.expiresAt
|
|
2423
|
+
}, 201);
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
return nativeGrantErrorResponse(error);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
|
|
2429
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2430
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2431
|
+
}
|
|
2432
|
+
if (!serviceCredentialMatches(
|
|
2433
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2434
|
+
config.nativeServiceCredential
|
|
2435
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2436
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2437
|
+
const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
|
|
2438
|
+
if (ownerHeader === null || ownerHeader.length === 0) {
|
|
2439
|
+
return json({ error: NATIVE_GRANT_REQUIRED }, 401);
|
|
2440
|
+
}
|
|
2441
|
+
const grant = await store.nativeGrantForOwner(ownerHeader);
|
|
2442
|
+
if (grant.status !== "ok") {
|
|
2443
|
+
const status = grant.status === "unavailable" ? 503 : 401;
|
|
2444
|
+
const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
|
|
2445
|
+
return json({ error: code }, status);
|
|
2446
|
+
}
|
|
2447
|
+
return handler.fetch(request, {
|
|
2448
|
+
authInfo: {
|
|
2449
|
+
token: "native-service",
|
|
2450
|
+
clientId: "tinycloud-native",
|
|
2451
|
+
scopes: [],
|
|
2452
|
+
extra: { nativeOwnerDid: grant.ownerDid }
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
1622
2456
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
1623
2457
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
1624
2458
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -1730,9 +2564,77 @@ function callbackCors(response, openkeyHost) {
|
|
|
1730
2564
|
headers.set("vary", "Origin");
|
|
1731
2565
|
return new Response(response.body, { status: response.status, headers });
|
|
1732
2566
|
}
|
|
2567
|
+
async function readBoundedBodyText(request, maxBytes) {
|
|
2568
|
+
const contentLength = request.headers.get("content-length");
|
|
2569
|
+
if (contentLength !== null) {
|
|
2570
|
+
const parsedLength = Number(contentLength);
|
|
2571
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
|
|
2572
|
+
}
|
|
2573
|
+
if (request.body === null) return "";
|
|
2574
|
+
const reader = request.body.getReader();
|
|
2575
|
+
const chunks = [];
|
|
2576
|
+
let total = 0;
|
|
2577
|
+
try {
|
|
2578
|
+
while (true) {
|
|
2579
|
+
const { done, value } = await reader.read();
|
|
2580
|
+
if (done) break;
|
|
2581
|
+
total += value.byteLength;
|
|
2582
|
+
if (total > maxBytes) {
|
|
2583
|
+
await reader.cancel();
|
|
2584
|
+
return void 0;
|
|
2585
|
+
}
|
|
2586
|
+
chunks.push(value);
|
|
2587
|
+
}
|
|
2588
|
+
} finally {
|
|
2589
|
+
reader.releaseLock();
|
|
2590
|
+
}
|
|
2591
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2592
|
+
}
|
|
2593
|
+
function nativeGrantErrorResponse(error) {
|
|
2594
|
+
if (!(error instanceof NativeGrantValidationError)) {
|
|
2595
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2596
|
+
}
|
|
2597
|
+
const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
|
|
2598
|
+
return json({ error: error.code }, unavailable ? 503 : 401);
|
|
2599
|
+
}
|
|
1733
2600
|
function json(value, status = 200) {
|
|
1734
2601
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
1735
2602
|
}
|
|
2603
|
+
async function withBearerMethodsSupported(response) {
|
|
2604
|
+
if (response.status === 204 || response.body === null) return response;
|
|
2605
|
+
const metadata = await response.json();
|
|
2606
|
+
const headers = new Headers(response.headers);
|
|
2607
|
+
headers.delete("content-length");
|
|
2608
|
+
headers.delete("content-encoding");
|
|
2609
|
+
return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
|
|
2610
|
+
status: response.status,
|
|
2611
|
+
statusText: response.statusText,
|
|
2612
|
+
headers
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2615
|
+
function publicMetadataCors(response, request) {
|
|
2616
|
+
const headers = new Headers(response.headers);
|
|
2617
|
+
headers.set("access-control-allow-origin", "*");
|
|
2618
|
+
headers.set("access-control-allow-methods", "GET, OPTIONS");
|
|
2619
|
+
const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
|
|
2620
|
+
if (requestedHeaders !== null) {
|
|
2621
|
+
headers.set("access-control-allow-headers", requestedHeaders);
|
|
2622
|
+
}
|
|
2623
|
+
headers.set(
|
|
2624
|
+
"vary",
|
|
2625
|
+
requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
|
|
2626
|
+
);
|
|
2627
|
+
return new Response(response.body, { status: response.status, headers });
|
|
2628
|
+
}
|
|
2629
|
+
function publicText(text, contentType) {
|
|
2630
|
+
return new Response(text, {
|
|
2631
|
+
headers: {
|
|
2632
|
+
"content-type": contentType,
|
|
2633
|
+
"cache-control": "public, max-age=300",
|
|
2634
|
+
"x-content-type-options": "nosniff"
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
1736
2638
|
function methodNotAllowed(allow) {
|
|
1737
2639
|
return new Response("Method not allowed", { status: 405, headers: { allow } });
|
|
1738
2640
|
}
|