@tinycloud/mcp 0.8.3-beta.16 → 0.8.3-beta.18
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 +10 -2
- 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 +567 -18
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +566 -14
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +566 -18
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +2 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +565 -14
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +566 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +565 -14
- 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/dist/http-cli.js
CHANGED
|
@@ -276,12 +276,15 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
// src/remote-store.ts
|
|
279
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
279
|
+
import { createHmac, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
280
280
|
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
281
281
|
import { dirname, join } from "path";
|
|
282
282
|
import {
|
|
283
|
+
activateValidatedRuntimeDelegation,
|
|
284
|
+
deserializeDelegation,
|
|
283
285
|
InvalidRestoredSessionError,
|
|
284
286
|
RuntimeDelegationRejectedError,
|
|
287
|
+
serializeDelegation,
|
|
285
288
|
TinyCloudNode,
|
|
286
289
|
UnsupportedSessionRestoreError,
|
|
287
290
|
isCapabilitySubset,
|
|
@@ -290,11 +293,80 @@ import {
|
|
|
290
293
|
signedCompactUcanExpiry
|
|
291
294
|
} from "@tinycloud/node-sdk";
|
|
292
295
|
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
296
|
+
import { createOrReusePermissionRequest } from "@tinycloud/operations/artifacts";
|
|
297
|
+
import { withTinyCloudStateRoot } from "@tinycloud/operations/state";
|
|
293
298
|
import { invokeOperation } from "@tinycloud/operations";
|
|
294
299
|
import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
|
|
295
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
|
|
296
361
|
var PROFILE = "agent";
|
|
297
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
|
+
]);
|
|
298
370
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
299
371
|
var BOOTSTRAP_PERMISSIONS = [
|
|
300
372
|
{
|
|
@@ -336,6 +408,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
336
408
|
this.detail = detail;
|
|
337
409
|
}
|
|
338
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
|
+
};
|
|
339
420
|
var RemoteTenantStore = class {
|
|
340
421
|
#config;
|
|
341
422
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -348,9 +429,186 @@ var RemoteTenantStore = class {
|
|
|
348
429
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
349
430
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
350
431
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
432
|
+
#nativeIdentity;
|
|
433
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
351
434
|
constructor(config) {
|
|
352
435
|
this.#config = config;
|
|
353
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
|
+
}
|
|
354
612
|
tenantStateRoot(subject) {
|
|
355
613
|
return join(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
356
614
|
}
|
|
@@ -902,7 +1160,7 @@ var RemoteTenantStore = class {
|
|
|
902
1160
|
const expected = this.#signState(tenantId, nonce);
|
|
903
1161
|
const supplied = Buffer.from(signature, "base64url");
|
|
904
1162
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
905
|
-
if (supplied.length !== expectedBytes.length || !
|
|
1163
|
+
if (supplied.length !== expectedBytes.length || !timingSafeEqual2(supplied, expectedBytes)) {
|
|
906
1164
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
907
1165
|
}
|
|
908
1166
|
const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -953,6 +1211,123 @@ var RemoteTenantStore = class {
|
|
|
953
1211
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
954
1212
|
return url.toString();
|
|
955
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
|
+
}
|
|
956
1331
|
#tenantId(subject) {
|
|
957
1332
|
return createHmac("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
958
1333
|
}
|
|
@@ -1046,6 +1421,21 @@ function samePrincipal(left, right) {
|
|
|
1046
1421
|
function normalizeDid(value) {
|
|
1047
1422
|
return value.split("#", 1)[0];
|
|
1048
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
|
+
}
|
|
1049
1439
|
function isPermissionEntry(value) {
|
|
1050
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");
|
|
1051
1441
|
}
|
|
@@ -1076,6 +1466,23 @@ async function readJson(path) {
|
|
|
1076
1466
|
throw error;
|
|
1077
1467
|
}
|
|
1078
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
|
+
}
|
|
1079
1486
|
async function reusablePending(directory, expected) {
|
|
1080
1487
|
let names;
|
|
1081
1488
|
try {
|
|
@@ -1472,7 +1879,7 @@ function createJsonSchemaValidator() {
|
|
|
1472
1879
|
return new AjvJsonSchemaValidator();
|
|
1473
1880
|
}
|
|
1474
1881
|
function registerTinyCloudTools(server, selection, validator) {
|
|
1475
|
-
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)));
|
|
1476
1883
|
for (const binding of bindings) {
|
|
1477
1884
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
1478
1885
|
if (operation === void 0) {
|
|
@@ -1550,7 +1957,7 @@ function outcomeCode(result) {
|
|
|
1550
1957
|
// package.json
|
|
1551
1958
|
var package_default = {
|
|
1552
1959
|
name: "@tinycloud/mcp",
|
|
1553
|
-
version: "0.8.3-beta.
|
|
1960
|
+
version: "0.8.3-beta.18",
|
|
1554
1961
|
description: "TinyCloud delegated operations MCP server",
|
|
1555
1962
|
keywords: [
|
|
1556
1963
|
"tinycloud",
|
|
@@ -1621,9 +2028,9 @@ var package_default = {
|
|
|
1621
2028
|
dependencies: {
|
|
1622
2029
|
"@modelcontextprotocol/node": "2.0.0-beta.5",
|
|
1623
2030
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
1624
|
-
"@tinycloud/node-sdk": "3.8.0-beta.
|
|
2031
|
+
"@tinycloud/node-sdk": "3.8.0-beta.16",
|
|
1625
2032
|
"@tinycloud/node-sdk-wasm": "1.7.8",
|
|
1626
|
-
"@tinycloud/operations": "0.7.7-beta.
|
|
2033
|
+
"@tinycloud/operations": "0.7.7-beta.15",
|
|
1627
2034
|
jose: "6.2.4"
|
|
1628
2035
|
},
|
|
1629
2036
|
devDependencies: {
|
|
@@ -1650,12 +2057,16 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1650
2057
|
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
1651
2058
|
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1652
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
|
+
}
|
|
1653
2063
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1654
2064
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1655
2065
|
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1656
2066
|
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1657
2067
|
const plugin = createHostedSurface({
|
|
1658
2068
|
...config,
|
|
2069
|
+
nativeServiceCredential: void 0,
|
|
1659
2070
|
stateDir: join3(config.stateDir, "chatgpt"),
|
|
1660
2071
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1661
2072
|
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
@@ -1691,16 +2102,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1691
2102
|
resourceMetadataUrl
|
|
1692
2103
|
});
|
|
1693
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);
|
|
1694
2107
|
const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
|
|
1695
|
-
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] };
|
|
1696
2110
|
const { subject, ownerDids } = identity;
|
|
1697
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2111
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1698
2112
|
const validator = createJsonSchemaValidator();
|
|
1699
2113
|
const server = new McpServer2(
|
|
1700
2114
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1701
2115
|
{
|
|
1702
2116
|
jsonSchemaValidator: validator,
|
|
1703
|
-
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."
|
|
1704
2118
|
}
|
|
1705
2119
|
);
|
|
1706
2120
|
registerTinyCloudTools(server, {
|
|
@@ -1709,10 +2123,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1709
2123
|
explicitProfile: true,
|
|
1710
2124
|
allowOwnerProfile: false,
|
|
1711
2125
|
stateRoot,
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
+
} : {}
|
|
1714
2133
|
}, validator);
|
|
1715
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2134
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1716
2135
|
return server;
|
|
1717
2136
|
}, {
|
|
1718
2137
|
legacy: "stateless",
|
|
@@ -1843,7 +2262,8 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1843
2262
|
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
1844
2263
|
}
|
|
1845
2264
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1846
|
-
|
|
2265
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2266
|
+
const card = {
|
|
1847
2267
|
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
1848
2268
|
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
1849
2269
|
title: "TinyCloud MCP",
|
|
@@ -1852,7 +2272,38 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1852
2272
|
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
1853
2273
|
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
1854
2274
|
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
1855
|
-
}
|
|
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);
|
|
1856
2307
|
}
|
|
1857
2308
|
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
1858
2309
|
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
@@ -1950,6 +2401,73 @@ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`
|
|
|
1950
2401
|
);
|
|
1951
2402
|
}
|
|
1952
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
|
+
}
|
|
1953
2471
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
1954
2472
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
1955
2473
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -2061,6 +2579,39 @@ function callbackCors(response, openkeyHost) {
|
|
|
2061
2579
|
headers.set("vary", "Origin");
|
|
2062
2580
|
return new Response(response.body, { status: response.status, headers });
|
|
2063
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
|
+
}
|
|
2064
2615
|
function json(value, status = 200) {
|
|
2065
2616
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
2066
2617
|
}
|
|
@@ -2128,6 +2679,7 @@ async function main() {
|
|
|
2128
2679
|
approvalTtlSeconds: positiveInteger(process.env.TC_MCP_APPROVAL_TTL_SECONDS, 300),
|
|
2129
2680
|
delegationExpiry: process.env.TC_MCP_DELEGATION_EXPIRY ?? "1h",
|
|
2130
2681
|
allowedOrigins: commaList(process.env.TC_MCP_ALLOWED_ORIGINS),
|
|
2682
|
+
...process.env.TC_MCP_NATIVE_SERVICE_CREDENTIAL === void 0 ? {} : { nativeServiceCredential: process.env.TC_MCP_NATIVE_SERVICE_CREDENTIAL },
|
|
2131
2683
|
oauthMetadata,
|
|
2132
2684
|
introspection: {
|
|
2133
2685
|
endpoint: requiredUrl("TC_MCP_INTROSPECTION_URL").toString(),
|