@tinycloud/mcp 0.8.3-beta.16 → 0.8.3-beta.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +565 -16
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +564 -12
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +564 -16
- 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 +563 -12
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +564 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +563 -12
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +2 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/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 +1 -1
package/dist/http-cli.cjs
CHANGED
|
@@ -289,16 +289,85 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// src/remote-store.ts
|
|
292
|
-
var
|
|
292
|
+
var import_node_crypto3 = require("crypto");
|
|
293
293
|
var import_promises = require("fs/promises");
|
|
294
294
|
var import_node_path = require("path");
|
|
295
295
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
296
296
|
var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
|
|
297
|
+
var import_artifacts = require("@tinycloud/operations/artifacts");
|
|
298
|
+
var import_state = require("@tinycloud/operations/state");
|
|
297
299
|
var import_operations = require("@tinycloud/operations");
|
|
298
300
|
var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
|
|
299
301
|
var import_tc_apps = require("@tinycloud/operations/tc-apps");
|
|
302
|
+
|
|
303
|
+
// src/native-auth.ts
|
|
304
|
+
var import_node_crypto2 = require("crypto");
|
|
305
|
+
var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
|
|
306
|
+
var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
|
|
307
|
+
var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
308
|
+
var NATIVE_GRANT_REQUIRED = "native_grant_required";
|
|
309
|
+
var NATIVE_GRANT_EXPIRED = "native_grant_expired";
|
|
310
|
+
var NATIVE_GRANT_REJECTED = "native_grant_rejected";
|
|
311
|
+
var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
|
|
312
|
+
var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
|
|
313
|
+
var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
|
|
314
|
+
var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
|
|
315
|
+
function serviceCredentialMatches(supplied, configured) {
|
|
316
|
+
if (supplied === null || configured === void 0) return false;
|
|
317
|
+
const suppliedBytes = Buffer.from(supplied, "utf8");
|
|
318
|
+
const configuredBytes = Buffer.from(configured, "utf8");
|
|
319
|
+
return suppliedBytes.byteLength === configuredBytes.byteLength && (0, import_node_crypto2.timingSafeEqual)(suppliedBytes, configuredBytes);
|
|
320
|
+
}
|
|
321
|
+
var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
|
|
322
|
+
var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
|
|
323
|
+
var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
|
|
324
|
+
function canonicalOwnerDid(value) {
|
|
325
|
+
const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
|
|
326
|
+
if (!match) return void 0;
|
|
327
|
+
const chainId = Number(match[1]);
|
|
328
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
329
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
330
|
+
}
|
|
331
|
+
function nativeOwnerForConnectorGrant(permissions) {
|
|
332
|
+
if (permissions.length !== 2) return void 0;
|
|
333
|
+
const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
|
|
334
|
+
const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
|
|
335
|
+
if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
|
|
336
|
+
const sql = sqlEntries[0];
|
|
337
|
+
const kv = kvEntries[0];
|
|
338
|
+
if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
|
|
339
|
+
if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
|
|
340
|
+
if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
|
|
341
|
+
const sqlSpace = ownerFromApplicationsSpace(sql.space);
|
|
342
|
+
const kvSpace = ownerFromApplicationsSpace(kv.space);
|
|
343
|
+
return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
|
|
344
|
+
}
|
|
345
|
+
function ownerFromApplicationsSpace(space) {
|
|
346
|
+
const match = OWNER_APPLICATIONS_SPACE.exec(space);
|
|
347
|
+
if (!match) return void 0;
|
|
348
|
+
const chainId = Number(match[1]);
|
|
349
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
350
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
351
|
+
}
|
|
352
|
+
function actionsEqual(actual, expected) {
|
|
353
|
+
const actualActions = [...actual].sort();
|
|
354
|
+
const expectedActions = [...expected].sort();
|
|
355
|
+
return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
|
|
356
|
+
}
|
|
357
|
+
function hasCaveats(permission) {
|
|
358
|
+
return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/remote-store.ts
|
|
300
362
|
var PROFILE = "agent";
|
|
301
363
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
364
|
+
var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
365
|
+
"DELEGATION_ARTIFACT_INVALID",
|
|
366
|
+
"DELEGATION_EXPIRED",
|
|
367
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
368
|
+
"DELEGATION_HOST_MISMATCH",
|
|
369
|
+
"DELEGATION_REJECTED"
|
|
370
|
+
]);
|
|
302
371
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
303
372
|
var BOOTSTRAP_PERMISSIONS = [
|
|
304
373
|
{
|
|
@@ -340,6 +409,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
340
409
|
this.detail = detail;
|
|
341
410
|
}
|
|
342
411
|
};
|
|
412
|
+
var NativeGrantValidationError = class extends Error {
|
|
413
|
+
code;
|
|
414
|
+
constructor(status) {
|
|
415
|
+
const code = status.startsWith("native_") ? status : status === "required" ? "native_grant_required" : status === "expired" ? "native_grant_expired" : status === "unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected";
|
|
416
|
+
super(code);
|
|
417
|
+
this.name = "NativeGrantValidationError";
|
|
418
|
+
this.code = code;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
343
421
|
var RemoteTenantStore = class {
|
|
344
422
|
#config;
|
|
345
423
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -352,9 +430,186 @@ var RemoteTenantStore = class {
|
|
|
352
430
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
353
431
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
354
432
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
433
|
+
#nativeIdentity;
|
|
434
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
355
435
|
constructor(config) {
|
|
356
436
|
this.#config = config;
|
|
357
437
|
}
|
|
438
|
+
async nativeServiceIdentity() {
|
|
439
|
+
const identity = await this.#loadNativeServiceKey();
|
|
440
|
+
return { serviceDid: normalizeDid(identity.did) };
|
|
441
|
+
}
|
|
442
|
+
nativeTenantStateRoot(ownerDid) {
|
|
443
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
444
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
445
|
+
const tenant = (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
|
|
446
|
+
return (0, import_node_path.join)(this.#config.stateDir, "native-tenants", tenant);
|
|
447
|
+
}
|
|
448
|
+
async acceptNativeGrant(serializedDelegation) {
|
|
449
|
+
if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
|
|
450
|
+
throw new NativeGrantValidationError("rejected");
|
|
451
|
+
}
|
|
452
|
+
const identity = await this.#loadNativeServiceKey();
|
|
453
|
+
const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
|
|
454
|
+
const { ownerDid, spaceId, permissions, expiresAt } = activated;
|
|
455
|
+
return this.#withNativeGrantLock(ownerDid, async () => {
|
|
456
|
+
const stateRoot = this.nativeTenantStateRoot(ownerDid);
|
|
457
|
+
let previous = null;
|
|
458
|
+
try {
|
|
459
|
+
const stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
460
|
+
if (isRecord(stored) && stored.version === 1 && stored.ownerDid === ownerDid && typeof stored.serviceDid === "string" && samePrincipal(stored.serviceDid, identity.did) && typeof stored.serializedDelegation === "string" && typeof stored.delegationCid === "string" && typeof stored.spaceId === "string" && Array.isArray(stored.permissions) && stored.permissions.every(isPermissionEntry) && nativeOwnerForConnectorGrant(stored.permissions) === ownerDid && typeof stored.expiresAt === "string" && Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
461
|
+
previous = stored;
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
465
|
+
}
|
|
466
|
+
if (previous !== null) {
|
|
467
|
+
const previousExpiry = Date.parse(previous.expiresAt);
|
|
468
|
+
const nextExpiry = expiresAt.getTime();
|
|
469
|
+
if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
|
|
470
|
+
throw new NativeGrantValidationError("rejected");
|
|
471
|
+
}
|
|
472
|
+
if (previous.delegationCid === activated.cid) {
|
|
473
|
+
return {
|
|
474
|
+
ownerDid,
|
|
475
|
+
serviceDid: normalizeDid(identity.did),
|
|
476
|
+
expiresAt: previous.expiresAt,
|
|
477
|
+
stateRoot
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const session = {
|
|
482
|
+
delegationHeader: activated.delegation.delegationHeader,
|
|
483
|
+
delegationCid: activated.cid,
|
|
484
|
+
spaceId,
|
|
485
|
+
jwk: identity.jwk,
|
|
486
|
+
verificationMethod: identity.did,
|
|
487
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
488
|
+
};
|
|
489
|
+
const grant = {
|
|
490
|
+
version: 1,
|
|
491
|
+
ownerDid,
|
|
492
|
+
serviceDid: normalizeDid(identity.did),
|
|
493
|
+
serializedDelegation: (0, import_node_sdk2.serializeDelegation)(activated.delegation),
|
|
494
|
+
delegationCid: activated.cid,
|
|
495
|
+
spaceId,
|
|
496
|
+
permissions,
|
|
497
|
+
expiresAt: expiresAt.toISOString()
|
|
498
|
+
};
|
|
499
|
+
const sessionPath = this.#sessionPath(stateRoot);
|
|
500
|
+
const previousSession = await readOptionalText(sessionPath);
|
|
501
|
+
try {
|
|
502
|
+
await secureWrite(this.#profilePath(stateRoot), {
|
|
503
|
+
name: PROFILE,
|
|
504
|
+
host: this.#config.nodeHost,
|
|
505
|
+
chainId: activated.delegation.chainId,
|
|
506
|
+
spaceName: "applications",
|
|
507
|
+
did: identity.did,
|
|
508
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
509
|
+
profile: PROFILE,
|
|
510
|
+
operatorType: "agent",
|
|
511
|
+
posture: "delegate-session",
|
|
512
|
+
ownerDid,
|
|
513
|
+
sessionDid: identity.did,
|
|
514
|
+
spaceId,
|
|
515
|
+
tinycloudHost: this.#config.nodeHost
|
|
516
|
+
});
|
|
517
|
+
await secureWrite(this.#keyPath(stateRoot), identity.jwk);
|
|
518
|
+
await secureWrite(sessionPath, session);
|
|
519
|
+
const request = await (0, import_state.withTinyCloudStateRoot)(stateRoot, () => (0, import_artifacts.createOrReusePermissionRequest)({
|
|
520
|
+
profile: PROFILE,
|
|
521
|
+
posture: "delegate-session",
|
|
522
|
+
operatorType: "agent",
|
|
523
|
+
host: this.#config.nodeHost,
|
|
524
|
+
sessionDid: identity.did,
|
|
525
|
+
ownerDid,
|
|
526
|
+
spaceId,
|
|
527
|
+
missing: permissions,
|
|
528
|
+
granted: [],
|
|
529
|
+
replace: true
|
|
530
|
+
}));
|
|
531
|
+
if (request.status !== "created") {
|
|
532
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
533
|
+
}
|
|
534
|
+
const imported = await (0, import_operations.invokeOperation)(
|
|
535
|
+
"tinycloud.auth.import",
|
|
536
|
+
1,
|
|
537
|
+
{ profile: PROFILE, stateRoot },
|
|
538
|
+
{
|
|
539
|
+
kind: "tinycloud.auth.delegation",
|
|
540
|
+
version: 1,
|
|
541
|
+
requestId: request.request.requestId,
|
|
542
|
+
delegationCid: activated.cid,
|
|
543
|
+
delegation: activated.delegation
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
if (imported.status === "error") {
|
|
547
|
+
throw new NativeGrantValidationError(
|
|
548
|
+
NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
|
|
552
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
553
|
+
}
|
|
554
|
+
await secureWrite(this.#nativeGrantPath(stateRoot), grant);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
try {
|
|
557
|
+
await restoreOptionalText(sessionPath, previousSession);
|
|
558
|
+
} catch {
|
|
559
|
+
await (0, import_promises.rm)(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
|
|
560
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
561
|
+
}
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async nativeGrantForOwner(ownerDid) {
|
|
568
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
569
|
+
if (canonical === void 0) return { status: "rejected" };
|
|
570
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
571
|
+
let stored;
|
|
572
|
+
try {
|
|
573
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
574
|
+
} catch {
|
|
575
|
+
return { status: "rejected" };
|
|
576
|
+
}
|
|
577
|
+
if (stored === null) return { status: "required" };
|
|
578
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.serviceDid !== "string" || typeof stored.serializedDelegation !== "string" || typeof stored.delegationCid !== "string" || typeof stored.spaceId !== "string" || typeof stored.expiresAt !== "string" || !Array.isArray(stored.permissions) || !stored.permissions.every(isPermissionEntry) || nativeOwnerForConnectorGrant(stored.permissions) !== canonical) {
|
|
579
|
+
return { status: "rejected" };
|
|
580
|
+
}
|
|
581
|
+
const expires = Date.parse(stored.expiresAt);
|
|
582
|
+
if (!Number.isFinite(expires)) return { status: "rejected" };
|
|
583
|
+
if (expires <= Date.now()) return { status: "expired" };
|
|
584
|
+
try {
|
|
585
|
+
const identity = await this.#loadNativeServiceKey();
|
|
586
|
+
if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
|
|
587
|
+
const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
|
|
588
|
+
if (activated.ownerDid !== canonical || activated.cid !== stored.delegationCid || activated.spaceId !== stored.spaceId || JSON.stringify(activated.permissions) !== JSON.stringify(stored.permissions) || activated.expiresAt.toISOString() !== stored.expiresAt) return { status: "rejected" };
|
|
589
|
+
return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (error instanceof NativeGrantValidationError) {
|
|
592
|
+
return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
|
|
593
|
+
}
|
|
594
|
+
return { status: "unavailable" };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async assertNativeGrantActive(ownerDid) {
|
|
598
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
599
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
600
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
601
|
+
let stored;
|
|
602
|
+
try {
|
|
603
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
604
|
+
} catch {
|
|
605
|
+
throw new NativeGrantValidationError("rejected");
|
|
606
|
+
}
|
|
607
|
+
if (stored === null) throw new NativeGrantValidationError("required");
|
|
608
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
609
|
+
throw new NativeGrantValidationError("rejected");
|
|
610
|
+
}
|
|
611
|
+
if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
|
|
612
|
+
}
|
|
358
613
|
tenantStateRoot(subject) {
|
|
359
614
|
return (0, import_node_path.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
360
615
|
}
|
|
@@ -881,7 +1136,7 @@ var RemoteTenantStore = class {
|
|
|
881
1136
|
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
882
1137
|
);
|
|
883
1138
|
if (reusable !== void 0) return reusable;
|
|
884
|
-
const nonce = (0,
|
|
1139
|
+
const nonce = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
|
|
885
1140
|
const created = Date.now();
|
|
886
1141
|
const pending = {
|
|
887
1142
|
version: 1,
|
|
@@ -906,7 +1161,7 @@ var RemoteTenantStore = class {
|
|
|
906
1161
|
const expected = this.#signState(tenantId, nonce);
|
|
907
1162
|
const supplied = Buffer.from(signature, "base64url");
|
|
908
1163
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
909
|
-
if (supplied.length !== expectedBytes.length || !(0,
|
|
1164
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto3.timingSafeEqual)(supplied, expectedBytes)) {
|
|
910
1165
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
911
1166
|
}
|
|
912
1167
|
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -957,11 +1212,128 @@ var RemoteTenantStore = class {
|
|
|
957
1212
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
958
1213
|
return url.toString();
|
|
959
1214
|
}
|
|
1215
|
+
async #withNativeGrantLock(ownerDid, action) {
|
|
1216
|
+
const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
|
|
1217
|
+
let releaseNext;
|
|
1218
|
+
const next = new Promise((resolve2) => {
|
|
1219
|
+
releaseNext = resolve2;
|
|
1220
|
+
});
|
|
1221
|
+
this.#nativeGrantQueues.set(ownerDid, next);
|
|
1222
|
+
await previous;
|
|
1223
|
+
try {
|
|
1224
|
+
return await action();
|
|
1225
|
+
} finally {
|
|
1226
|
+
releaseNext();
|
|
1227
|
+
if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async #loadNativeServiceKey() {
|
|
1231
|
+
if (this.#nativeIdentity === void 0) {
|
|
1232
|
+
const loading = this.#readOrCreateNativeServiceKey();
|
|
1233
|
+
this.#nativeIdentity = loading;
|
|
1234
|
+
void loading.catch(() => {
|
|
1235
|
+
if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
return this.#nativeIdentity;
|
|
1239
|
+
}
|
|
1240
|
+
async #readOrCreateNativeServiceKey() {
|
|
1241
|
+
const path = (0, import_node_path.join)(this.#config.stateDir, ".tinycloud", "native-service-key.json");
|
|
1242
|
+
await secureDirectory((0, import_node_path.dirname)(path));
|
|
1243
|
+
let stored = await readJson(path);
|
|
1244
|
+
if (stored === null) {
|
|
1245
|
+
const generated = generateSessionKey();
|
|
1246
|
+
try {
|
|
1247
|
+
await (0, import_promises.writeFile)(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
|
|
1248
|
+
`, {
|
|
1249
|
+
mode: 384,
|
|
1250
|
+
flag: "wx"
|
|
1251
|
+
});
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
if (!isRecord(error) || error.code !== "EEXIST") throw error;
|
|
1254
|
+
}
|
|
1255
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1256
|
+
stored = await readJson(path);
|
|
1257
|
+
}
|
|
1258
|
+
if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
|
|
1259
|
+
throw new Error("The native MCP service identity is unavailable.");
|
|
1260
|
+
}
|
|
1261
|
+
return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
|
|
1262
|
+
}
|
|
1263
|
+
async #activateNativeDelegation(serialized, identity) {
|
|
1264
|
+
let delegation;
|
|
1265
|
+
try {
|
|
1266
|
+
const parsed = JSON.parse(serialized);
|
|
1267
|
+
if (!isRecord(parsed)) throw new Error("invalid delegation");
|
|
1268
|
+
delegation = (0, import_node_sdk2.deserializeDelegation)(serialized);
|
|
1269
|
+
} catch {
|
|
1270
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1271
|
+
}
|
|
1272
|
+
if (!isRecord(delegation.delegationHeader) || typeof delegation.delegationHeader.Authorization !== "string" || delegation.delegationHeader.Authorization.length === 0 || typeof delegation.cid !== "string" || delegation.cid.length === 0 || typeof delegation.delegateDID !== "string" || !samePrincipal(delegation.delegateDID, normalizeDid(identity.did)) || typeof delegation.spaceId !== "string" || typeof delegation.ownerAddress !== "string" || !Number.isSafeInteger(delegation.chainId) || typeof delegation.host !== "string" || !sameNodeHost(delegation.host, this.#config.nodeHost) || !(delegation.expiry instanceof Date) || !Number.isFinite(delegation.expiry.getTime())) {
|
|
1273
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1274
|
+
}
|
|
1275
|
+
const now = Date.now();
|
|
1276
|
+
const claimedExpiry = delegation.expiry.getTime();
|
|
1277
|
+
if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
|
|
1278
|
+
if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1279
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1280
|
+
}
|
|
1281
|
+
let ownerDid;
|
|
1282
|
+
let spaceId;
|
|
1283
|
+
const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
|
|
1284
|
+
try {
|
|
1285
|
+
await node.restoreSession({
|
|
1286
|
+
delegationHeader: delegation.delegationHeader,
|
|
1287
|
+
delegationCid: delegation.cid,
|
|
1288
|
+
spaceId: delegation.spaceId,
|
|
1289
|
+
jwk: identity.jwk,
|
|
1290
|
+
verificationMethod: identity.did,
|
|
1291
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1292
|
+
});
|
|
1293
|
+
const activated = await (0, import_node_sdk2.activateValidatedRuntimeDelegation)(node, delegation, {
|
|
1294
|
+
host: this.#config.nodeHost,
|
|
1295
|
+
authorize: (permissions) => {
|
|
1296
|
+
const derivedOwner = nativeOwnerForConnectorGrant(permissions);
|
|
1297
|
+
const signedSpace = permissions[0]?.space;
|
|
1298
|
+
if (derivedOwner === void 0 || signedSpace === void 0) return false;
|
|
1299
|
+
const metadataOwner = canonicalOwnerDid(
|
|
1300
|
+
`did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
|
|
1301
|
+
);
|
|
1302
|
+
if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
|
|
1303
|
+
ownerDid = derivedOwner;
|
|
1304
|
+
spaceId = signedSpace;
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1309
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
ownerDid,
|
|
1313
|
+
spaceId,
|
|
1314
|
+
permissions: [...activated.effectivePermissions],
|
|
1315
|
+
cid: activated.cid,
|
|
1316
|
+
expiresAt: activated.expiry,
|
|
1317
|
+
delegation: activated.delegation
|
|
1318
|
+
};
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
if (error instanceof NativeGrantValidationError) throw error;
|
|
1321
|
+
if (error instanceof import_node_sdk2.RuntimeDelegationRejectedError) {
|
|
1322
|
+
throw new NativeGrantValidationError(
|
|
1323
|
+
error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
#nativeGrantPath(stateRoot) {
|
|
1330
|
+
return (0, import_node_path.join)(stateRoot, ".tinycloud", "native-grant.json");
|
|
1331
|
+
}
|
|
960
1332
|
#tenantId(subject) {
|
|
961
|
-
return (0,
|
|
1333
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
962
1334
|
}
|
|
963
1335
|
#signState(tenantId, nonce) {
|
|
964
|
-
return (0,
|
|
1336
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
965
1337
|
}
|
|
966
1338
|
#profilePath(stateRoot) {
|
|
967
1339
|
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
@@ -1050,6 +1422,21 @@ function samePrincipal(left, right) {
|
|
|
1050
1422
|
function normalizeDid(value) {
|
|
1051
1423
|
return value.split("#", 1)[0];
|
|
1052
1424
|
}
|
|
1425
|
+
function sameNodeHost(left, right) {
|
|
1426
|
+
try {
|
|
1427
|
+
const normalize = (value) => {
|
|
1428
|
+
const url = new URL(value);
|
|
1429
|
+
if (url.username !== "" || url.password !== "") return "";
|
|
1430
|
+
url.hash = "";
|
|
1431
|
+
url.search = "";
|
|
1432
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
1433
|
+
return url.toString().replace(/\/+$/, "");
|
|
1434
|
+
};
|
|
1435
|
+
return normalize(left) !== "" && normalize(left) === normalize(right);
|
|
1436
|
+
} catch {
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1053
1440
|
function isPermissionEntry(value) {
|
|
1054
1441
|
return isRecord(value) && typeof value.service === "string" && typeof value.space === "string" && typeof value.path === "string" && Array.isArray(value.actions) && value.actions.every((action) => typeof action === "string");
|
|
1055
1442
|
}
|
|
@@ -1063,7 +1450,7 @@ async function secureDirectory(path) {
|
|
|
1063
1450
|
async function secureWrite(path, value) {
|
|
1064
1451
|
const directory = (0, import_node_path.dirname)(path);
|
|
1065
1452
|
await secureDirectory(directory);
|
|
1066
|
-
const temporary = `${path}.${process.pid}.${(0,
|
|
1453
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`;
|
|
1067
1454
|
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
1068
1455
|
`, { mode: 384 });
|
|
1069
1456
|
await (0, import_promises.rename)(temporary, path);
|
|
@@ -1080,6 +1467,23 @@ async function readJson(path) {
|
|
|
1080
1467
|
throw error;
|
|
1081
1468
|
}
|
|
1082
1469
|
}
|
|
1470
|
+
async function readOptionalText(path) {
|
|
1471
|
+
try {
|
|
1472
|
+
return await (0, import_promises.readFile)(path, "utf8");
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
1475
|
+
throw error;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
async function restoreOptionalText(path, contents) {
|
|
1479
|
+
if (contents === null) {
|
|
1480
|
+
await (0, import_promises.rm)(path, { force: true });
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
await secureDirectory((0, import_node_path.dirname)(path));
|
|
1484
|
+
await (0, import_promises.writeFile)(path, contents, { mode: 384 });
|
|
1485
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1486
|
+
}
|
|
1083
1487
|
async function reusablePending(directory, expected) {
|
|
1084
1488
|
let names;
|
|
1085
1489
|
try {
|
|
@@ -1477,7 +1881,7 @@ function createJsonSchemaValidator() {
|
|
|
1477
1881
|
return new import_ajv.AjvJsonSchemaValidator();
|
|
1478
1882
|
}
|
|
1479
1883
|
function registerTinyCloudTools(server, selection, validator) {
|
|
1480
|
-
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
|
|
1884
|
+
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
|
|
1481
1885
|
for (const binding of bindings) {
|
|
1482
1886
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
1483
1887
|
if (operation === void 0) {
|
|
@@ -1555,7 +1959,7 @@ function outcomeCode(result) {
|
|
|
1555
1959
|
// package.json
|
|
1556
1960
|
var package_default = {
|
|
1557
1961
|
name: "@tinycloud/mcp",
|
|
1558
|
-
version: "0.8.3-beta.
|
|
1962
|
+
version: "0.8.3-beta.17",
|
|
1559
1963
|
description: "TinyCloud delegated operations MCP server",
|
|
1560
1964
|
keywords: [
|
|
1561
1965
|
"tinycloud",
|
|
@@ -1655,12 +2059,16 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1655
2059
|
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
1656
2060
|
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1657
2061
|
function createHostedMcpApp(config) {
|
|
2062
|
+
if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
|
|
2063
|
+
throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
|
|
2064
|
+
}
|
|
1658
2065
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1659
2066
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1660
2067
|
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1661
2068
|
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1662
2069
|
const plugin = createHostedSurface({
|
|
1663
2070
|
...config,
|
|
2071
|
+
nativeServiceCredential: void 0,
|
|
1664
2072
|
stateDir: (0, import_node_path3.join)(config.stateDir, "chatgpt"),
|
|
1665
2073
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1666
2074
|
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
@@ -1696,16 +2104,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1696
2104
|
resourceMetadataUrl
|
|
1697
2105
|
});
|
|
1698
2106
|
const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
|
|
2107
|
+
const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
|
|
2108
|
+
if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
|
|
1699
2109
|
const handler = (0, import_server4.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
|
|
1700
|
-
const
|
|
2110
|
+
const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
|
|
2111
|
+
const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
|
|
1701
2112
|
const { subject, ownerDids } = identity;
|
|
1702
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2113
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1703
2114
|
const validator = createJsonSchemaValidator();
|
|
1704
2115
|
const server = new import_server4.McpServer(
|
|
1705
2116
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1706
2117
|
{
|
|
1707
2118
|
jsonSchemaValidator: validator,
|
|
1708
|
-
instructions: surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first."
|
|
2119
|
+
instructions: nativeOwner === void 0 ? surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first." : "Use only the user's requested connector-transcript data. The signed TinyCloud grant authorizes read-only SQL and transcript KV reads."
|
|
1709
2120
|
}
|
|
1710
2121
|
);
|
|
1711
2122
|
registerTinyCloudTools(server, {
|
|
@@ -1714,10 +2125,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1714
2125
|
explicitProfile: true,
|
|
1715
2126
|
allowOwnerProfile: false,
|
|
1716
2127
|
stateRoot,
|
|
1717
|
-
|
|
1718
|
-
|
|
2128
|
+
...nativeOwner === void 0 ? {} : {
|
|
2129
|
+
allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
|
|
2130
|
+
},
|
|
2131
|
+
beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
|
|
2132
|
+
...nativeOwner === void 0 ? {
|
|
2133
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
2134
|
+
} : {}
|
|
1719
2135
|
}, validator);
|
|
1720
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2136
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1721
2137
|
return server;
|
|
1722
2138
|
}, {
|
|
1723
2139
|
legacy: "stateless",
|
|
@@ -1848,7 +2264,8 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1848
2264
|
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
1849
2265
|
}
|
|
1850
2266
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1851
|
-
|
|
2267
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2268
|
+
const card = {
|
|
1852
2269
|
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
1853
2270
|
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
1854
2271
|
title: "TinyCloud MCP",
|
|
@@ -1857,7 +2274,38 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1857
2274
|
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
1858
2275
|
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
1859
2276
|
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
1860
|
-
}
|
|
2277
|
+
};
|
|
2278
|
+
if (identity !== void 0) {
|
|
2279
|
+
card["x-tinycloud-native-auth"] = {
|
|
2280
|
+
serviceDid: identity.serviceDid,
|
|
2281
|
+
serviceDidField: "x-tinycloud-native-auth.serviceDid",
|
|
2282
|
+
grantEndpoint: {
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
path: "/native/grant",
|
|
2285
|
+
body: { delegation: "serialized PortableDelegation JSON string" }
|
|
2286
|
+
},
|
|
2287
|
+
serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
|
|
2288
|
+
serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
|
|
2289
|
+
ownerDidHeader: NATIVE_OWNER_DID_HEADER,
|
|
2290
|
+
maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
|
|
2291
|
+
acceptedCapabilities: [
|
|
2292
|
+
{ service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
|
|
2293
|
+
{ service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
|
|
2294
|
+
],
|
|
2295
|
+
tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
|
|
2296
|
+
errors: {
|
|
2297
|
+
required: NATIVE_GRANT_REQUIRED,
|
|
2298
|
+
expired: NATIVE_GRANT_EXPIRED,
|
|
2299
|
+
rejected: NATIVE_GRANT_REJECTED,
|
|
2300
|
+
serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
|
|
2301
|
+
serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
|
|
2302
|
+
validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
return publicMetadataCors(Response.json(card, {
|
|
2307
|
+
headers: { "cache-control": "public, max-age=300" }
|
|
2308
|
+
}), request);
|
|
1861
2309
|
}
|
|
1862
2310
|
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
1863
2311
|
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
@@ -1955,6 +2403,73 @@ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`
|
|
|
1955
2403
|
);
|
|
1956
2404
|
}
|
|
1957
2405
|
}
|
|
2406
|
+
if (url.pathname === "/native/grant" && surface === "hosted") {
|
|
2407
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
2408
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2409
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2410
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2411
|
+
}
|
|
2412
|
+
if (!serviceCredentialMatches(
|
|
2413
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2414
|
+
config.nativeServiceCredential
|
|
2415
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2416
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
2417
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2418
|
+
}
|
|
2419
|
+
const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
|
|
2420
|
+
if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2421
|
+
let body;
|
|
2422
|
+
try {
|
|
2423
|
+
body = JSON.parse(bodyText);
|
|
2424
|
+
} catch {
|
|
2425
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2426
|
+
}
|
|
2427
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
|
|
2428
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2429
|
+
}
|
|
2430
|
+
const grantBody = body;
|
|
2431
|
+
if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
|
|
2432
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
const accepted = await store.acceptNativeGrant(grantBody.delegation);
|
|
2436
|
+
return json({
|
|
2437
|
+
ownerDid: accepted.ownerDid,
|
|
2438
|
+
serviceDid: accepted.serviceDid,
|
|
2439
|
+
expiresAt: accepted.expiresAt
|
|
2440
|
+
}, 201);
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
return nativeGrantErrorResponse(error);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
|
|
2446
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2447
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2448
|
+
}
|
|
2449
|
+
if (!serviceCredentialMatches(
|
|
2450
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2451
|
+
config.nativeServiceCredential
|
|
2452
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2453
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2454
|
+
const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
|
|
2455
|
+
if (ownerHeader === null || ownerHeader.length === 0) {
|
|
2456
|
+
return json({ error: NATIVE_GRANT_REQUIRED }, 401);
|
|
2457
|
+
}
|
|
2458
|
+
const grant = await store.nativeGrantForOwner(ownerHeader);
|
|
2459
|
+
if (grant.status !== "ok") {
|
|
2460
|
+
const status = grant.status === "unavailable" ? 503 : 401;
|
|
2461
|
+
const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
|
|
2462
|
+
return json({ error: code }, status);
|
|
2463
|
+
}
|
|
2464
|
+
return handler.fetch(request, {
|
|
2465
|
+
authInfo: {
|
|
2466
|
+
token: "native-service",
|
|
2467
|
+
clientId: "tinycloud-native",
|
|
2468
|
+
scopes: [],
|
|
2469
|
+
extra: { nativeOwnerDid: grant.ownerDid }
|
|
2470
|
+
}
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
1958
2473
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
1959
2474
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
1960
2475
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -2066,6 +2581,39 @@ function callbackCors(response, openkeyHost) {
|
|
|
2066
2581
|
headers.set("vary", "Origin");
|
|
2067
2582
|
return new Response(response.body, { status: response.status, headers });
|
|
2068
2583
|
}
|
|
2584
|
+
async function readBoundedBodyText(request, maxBytes) {
|
|
2585
|
+
const contentLength = request.headers.get("content-length");
|
|
2586
|
+
if (contentLength !== null) {
|
|
2587
|
+
const parsedLength = Number(contentLength);
|
|
2588
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
|
|
2589
|
+
}
|
|
2590
|
+
if (request.body === null) return "";
|
|
2591
|
+
const reader = request.body.getReader();
|
|
2592
|
+
const chunks = [];
|
|
2593
|
+
let total = 0;
|
|
2594
|
+
try {
|
|
2595
|
+
while (true) {
|
|
2596
|
+
const { done, value } = await reader.read();
|
|
2597
|
+
if (done) break;
|
|
2598
|
+
total += value.byteLength;
|
|
2599
|
+
if (total > maxBytes) {
|
|
2600
|
+
await reader.cancel();
|
|
2601
|
+
return void 0;
|
|
2602
|
+
}
|
|
2603
|
+
chunks.push(value);
|
|
2604
|
+
}
|
|
2605
|
+
} finally {
|
|
2606
|
+
reader.releaseLock();
|
|
2607
|
+
}
|
|
2608
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2609
|
+
}
|
|
2610
|
+
function nativeGrantErrorResponse(error) {
|
|
2611
|
+
if (!(error instanceof NativeGrantValidationError)) {
|
|
2612
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2613
|
+
}
|
|
2614
|
+
const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
|
|
2615
|
+
return json({ error: error.code }, unavailable ? 503 : 401);
|
|
2616
|
+
}
|
|
2069
2617
|
function json(value, status = 200) {
|
|
2070
2618
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
2071
2619
|
}
|
|
@@ -2133,6 +2681,7 @@ async function main() {
|
|
|
2133
2681
|
approvalTtlSeconds: positiveInteger(process.env.TC_MCP_APPROVAL_TTL_SECONDS, 300),
|
|
2134
2682
|
delegationExpiry: process.env.TC_MCP_DELEGATION_EXPIRY ?? "1h",
|
|
2135
2683
|
allowedOrigins: commaList(process.env.TC_MCP_ALLOWED_ORIGINS),
|
|
2684
|
+
...process.env.TC_MCP_NATIVE_SERVICE_CREDENTIAL === void 0 ? {} : { nativeServiceCredential: process.env.TC_MCP_NATIVE_SERVICE_CREDENTIAL },
|
|
2136
2685
|
oauthMetadata,
|
|
2137
2686
|
introspection: {
|
|
2138
2687
|
endpoint: requiredUrl("TC_MCP_INTROSPECTION_URL").toString(),
|