@tinycloud/mcp 0.8.3-beta.2 → 0.8.3-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -44
- package/dist/cli.cjs +4 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +940 -26
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +939 -22
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +927 -25
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +24 -0
- package/dist/http.d.ts +24 -0
- package/dist/http.js +926 -21
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +927 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +926 -21
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +4 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +3 -1
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +3 -3
- package/skills/tinycloud-delegated-secrets/SKILL.md +18 -4
package/dist/index.js
CHANGED
|
@@ -407,7 +407,7 @@ function createJsonSchemaValidator() {
|
|
|
407
407
|
return new AjvJsonSchemaValidator();
|
|
408
408
|
}
|
|
409
409
|
function registerTinyCloudTools(server, selection, validator) {
|
|
410
|
-
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
|
|
410
|
+
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
|
|
411
411
|
for (const binding of bindings) {
|
|
412
412
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
413
413
|
if (operation === void 0) {
|
|
@@ -485,7 +485,7 @@ function outcomeCode(result) {
|
|
|
485
485
|
// package.json
|
|
486
486
|
var package_default = {
|
|
487
487
|
name: "@tinycloud/mcp",
|
|
488
|
-
version: "0.8.3-beta.
|
|
488
|
+
version: "0.8.3-beta.21",
|
|
489
489
|
description: "TinyCloud delegated operations MCP server",
|
|
490
490
|
keywords: [
|
|
491
491
|
"tinycloud",
|
|
@@ -556,9 +556,9 @@ var package_default = {
|
|
|
556
556
|
dependencies: {
|
|
557
557
|
"@modelcontextprotocol/node": "2.0.0-beta.5",
|
|
558
558
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
559
|
-
"@tinycloud/node-sdk": "3.8.0-beta.
|
|
559
|
+
"@tinycloud/node-sdk": "3.8.0-beta.19",
|
|
560
560
|
"@tinycloud/node-sdk-wasm": "1.7.8",
|
|
561
|
-
"@tinycloud/operations": "0.7.7-beta.
|
|
561
|
+
"@tinycloud/operations": "0.7.7-beta.18",
|
|
562
562
|
jose: "6.2.4"
|
|
563
563
|
},
|
|
564
564
|
devDependencies: {
|
|
@@ -726,7 +726,9 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
726
726
|
issuer: config.issuer,
|
|
727
727
|
audience: config.audience
|
|
728
728
|
});
|
|
729
|
-
|
|
729
|
+
const info = authInfo(token, payload, config);
|
|
730
|
+
if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
|
|
731
|
+
return info;
|
|
730
732
|
} catch (error) {
|
|
731
733
|
if (error instanceof errors.JOSEError) {
|
|
732
734
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
@@ -736,6 +738,16 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
736
738
|
}
|
|
737
739
|
};
|
|
738
740
|
}
|
|
741
|
+
async function requireUsableStatus(checker, token) {
|
|
742
|
+
const status = await checker.tokenStatus(token);
|
|
743
|
+
if (status === "active") return;
|
|
744
|
+
if (status === "inactive") {
|
|
745
|
+
throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
|
|
746
|
+
}
|
|
747
|
+
if (!checker.toleratesUnknown(token)) {
|
|
748
|
+
throw new OAuthError(OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
|
|
749
|
+
}
|
|
750
|
+
}
|
|
739
751
|
function authInfo(token, payload, config) {
|
|
740
752
|
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
741
753
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token does not identify a subject.");
|
|
@@ -789,13 +801,160 @@ function firstString(...values) {
|
|
|
789
801
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
790
802
|
}
|
|
791
803
|
|
|
804
|
+
// src/token-status.ts
|
|
805
|
+
import { createHash } from "crypto";
|
|
806
|
+
import { decodeJwt } from "jose";
|
|
807
|
+
var ACTIVE_CACHE_MS = 3e4;
|
|
808
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
809
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
810
|
+
var MAX_CACHED_TOKENS = 1e4;
|
|
811
|
+
function createTokenStatusChecker(config) {
|
|
812
|
+
const doFetch = config.fetch ?? fetch;
|
|
813
|
+
const clock = config.now ?? Date.now;
|
|
814
|
+
const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
815
|
+
`));
|
|
816
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
817
|
+
const inactive = /* @__PURE__ */ new Map();
|
|
818
|
+
const active = /* @__PURE__ */ new Map();
|
|
819
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
820
|
+
const confirmedActive = /* @__PURE__ */ new Map();
|
|
821
|
+
let credentialsRefused = false;
|
|
822
|
+
async function introspect(token) {
|
|
823
|
+
const headers = {
|
|
824
|
+
accept: "application/json",
|
|
825
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
826
|
+
};
|
|
827
|
+
const body = new URLSearchParams({ token, token_type_hint: "access_token" });
|
|
828
|
+
if (config.authMethod === "basic") {
|
|
829
|
+
const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
830
|
+
const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
|
|
831
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
832
|
+
} else {
|
|
833
|
+
body.set("client_id", config.clientId);
|
|
834
|
+
body.set("client_secret", config.clientSecret);
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
const response = await doFetch(config.endpoint, {
|
|
838
|
+
method: "POST",
|
|
839
|
+
headers,
|
|
840
|
+
body,
|
|
841
|
+
redirect: "error",
|
|
842
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
843
|
+
});
|
|
844
|
+
if (response.status === 401) return "credentials";
|
|
845
|
+
if (response.status !== 200) return "unknown";
|
|
846
|
+
const result = await response.json();
|
|
847
|
+
if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
|
|
848
|
+
if (result.active === false) return "inactive";
|
|
849
|
+
if (result.active !== true) return "unknown";
|
|
850
|
+
return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
|
|
851
|
+
} catch {
|
|
852
|
+
return "unknown";
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async function query(key, token) {
|
|
856
|
+
const outcome = await introspect(token);
|
|
857
|
+
if (outcome === "credentials") {
|
|
858
|
+
if (!credentialsRefused) {
|
|
859
|
+
log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
|
|
860
|
+
}
|
|
861
|
+
credentialsRefused = true;
|
|
862
|
+
return "unknown";
|
|
863
|
+
}
|
|
864
|
+
if (outcome === "unknown") return outcome;
|
|
865
|
+
const now = clock();
|
|
866
|
+
prune(now);
|
|
867
|
+
if (outcome === "mismatch") {
|
|
868
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
869
|
+
confirmedActive.delete(key);
|
|
870
|
+
return "inactive";
|
|
871
|
+
}
|
|
872
|
+
credentialsRefused = false;
|
|
873
|
+
if (outcome === "inactive") {
|
|
874
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
875
|
+
confirmedActive.delete(key);
|
|
876
|
+
} else {
|
|
877
|
+
active.set(key, now + ACTIVE_CACHE_MS);
|
|
878
|
+
confirmedActive.delete(key);
|
|
879
|
+
confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
|
|
880
|
+
}
|
|
881
|
+
return outcome;
|
|
882
|
+
}
|
|
883
|
+
function prune(now) {
|
|
884
|
+
for (const [key, until] of active) if (until <= now) active.delete(key);
|
|
885
|
+
for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
|
|
886
|
+
for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
|
|
887
|
+
for (const cache of [active, inactive, confirmedActive]) {
|
|
888
|
+
while (cache.size >= MAX_CACHED_TOKENS) {
|
|
889
|
+
const oldest = cache.keys().next().value;
|
|
890
|
+
if (oldest === void 0) break;
|
|
891
|
+
cache.delete(oldest);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
async tokenStatus(token) {
|
|
897
|
+
const key = createHash("sha256").update(token).digest("hex");
|
|
898
|
+
const now = clock();
|
|
899
|
+
const inactiveUntil = inactive.get(key);
|
|
900
|
+
if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
|
|
901
|
+
const activeUntil = active.get(key);
|
|
902
|
+
if (activeUntil !== void 0 && activeUntil > now) return "active";
|
|
903
|
+
const pending = inFlight.get(key);
|
|
904
|
+
if (pending !== void 0) return pending;
|
|
905
|
+
const asking = query(key, token).finally(() => inFlight.delete(key));
|
|
906
|
+
inFlight.set(key, asking);
|
|
907
|
+
return asking;
|
|
908
|
+
},
|
|
909
|
+
toleratesUnknown(token) {
|
|
910
|
+
if (credentialsRefused) return false;
|
|
911
|
+
const confirmed = confirmedActive.get(createHash("sha256").update(token).digest("hex"));
|
|
912
|
+
if (confirmed === void 0) return false;
|
|
913
|
+
const now = clock();
|
|
914
|
+
return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function answerMatchesToken(answer, token, resourceUrl) {
|
|
919
|
+
const jwt = claims(token);
|
|
920
|
+
if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
|
|
921
|
+
const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
|
|
922
|
+
if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
|
|
923
|
+
if (jwt.aud === void 0) return true;
|
|
924
|
+
const answered = audienceList(answer.aud);
|
|
925
|
+
const signed = audienceList(jwt.aud);
|
|
926
|
+
return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
|
|
927
|
+
}
|
|
928
|
+
function audienceList(value) {
|
|
929
|
+
const list = typeof value === "string" ? [value] : value;
|
|
930
|
+
return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
|
|
931
|
+
}
|
|
932
|
+
function tokenExpiryMs(token, now) {
|
|
933
|
+
const exp = claims(token).exp;
|
|
934
|
+
return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
|
|
935
|
+
}
|
|
936
|
+
function tokenLifetimeSeconds(token) {
|
|
937
|
+
const { exp, iat } = claims(token);
|
|
938
|
+
return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
939
|
+
}
|
|
940
|
+
function claims(token) {
|
|
941
|
+
try {
|
|
942
|
+
return decodeJwt(token);
|
|
943
|
+
} catch {
|
|
944
|
+
return {};
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
792
948
|
// src/remote-store.ts
|
|
793
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
949
|
+
import { createHmac, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
794
950
|
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
795
951
|
import { dirname, join as join2 } from "path";
|
|
796
952
|
import {
|
|
953
|
+
activateValidatedRuntimeDelegation,
|
|
954
|
+
deserializeDelegation,
|
|
797
955
|
InvalidRestoredSessionError,
|
|
798
956
|
RuntimeDelegationRejectedError,
|
|
957
|
+
serializeDelegation,
|
|
799
958
|
TinyCloudNode,
|
|
800
959
|
UnsupportedSessionRestoreError,
|
|
801
960
|
isCapabilitySubset,
|
|
@@ -804,11 +963,80 @@ import {
|
|
|
804
963
|
signedCompactUcanExpiry
|
|
805
964
|
} from "@tinycloud/node-sdk";
|
|
806
965
|
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
966
|
+
import { createOrReusePermissionRequest } from "@tinycloud/operations/artifacts";
|
|
967
|
+
import { withTinyCloudStateRoot } from "@tinycloud/operations/state";
|
|
807
968
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
808
969
|
import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
|
|
809
970
|
import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
|
|
971
|
+
|
|
972
|
+
// src/native-auth.ts
|
|
973
|
+
import { timingSafeEqual } from "crypto";
|
|
974
|
+
var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
|
|
975
|
+
var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
|
|
976
|
+
var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
977
|
+
var NATIVE_GRANT_REQUIRED = "native_grant_required";
|
|
978
|
+
var NATIVE_GRANT_EXPIRED = "native_grant_expired";
|
|
979
|
+
var NATIVE_GRANT_REJECTED = "native_grant_rejected";
|
|
980
|
+
var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
|
|
981
|
+
var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
|
|
982
|
+
var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
|
|
983
|
+
var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
|
|
984
|
+
function serviceCredentialMatches(supplied, configured) {
|
|
985
|
+
if (supplied === null || configured === void 0) return false;
|
|
986
|
+
const suppliedBytes = Buffer.from(supplied, "utf8");
|
|
987
|
+
const configuredBytes = Buffer.from(configured, "utf8");
|
|
988
|
+
return suppliedBytes.byteLength === configuredBytes.byteLength && timingSafeEqual(suppliedBytes, configuredBytes);
|
|
989
|
+
}
|
|
990
|
+
var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
|
|
991
|
+
var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
|
|
992
|
+
var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
|
|
993
|
+
function canonicalOwnerDid(value) {
|
|
994
|
+
const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
|
|
995
|
+
if (!match) return void 0;
|
|
996
|
+
const chainId = Number(match[1]);
|
|
997
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
998
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
999
|
+
}
|
|
1000
|
+
function nativeOwnerForConnectorGrant(permissions) {
|
|
1001
|
+
if (permissions.length !== 2) return void 0;
|
|
1002
|
+
const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
|
|
1003
|
+
const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
|
|
1004
|
+
if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
|
|
1005
|
+
const sql = sqlEntries[0];
|
|
1006
|
+
const kv = kvEntries[0];
|
|
1007
|
+
if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
|
|
1008
|
+
if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
|
|
1009
|
+
if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
|
|
1010
|
+
const sqlSpace = ownerFromApplicationsSpace(sql.space);
|
|
1011
|
+
const kvSpace = ownerFromApplicationsSpace(kv.space);
|
|
1012
|
+
return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
|
|
1013
|
+
}
|
|
1014
|
+
function ownerFromApplicationsSpace(space) {
|
|
1015
|
+
const match = OWNER_APPLICATIONS_SPACE.exec(space);
|
|
1016
|
+
if (!match) return void 0;
|
|
1017
|
+
const chainId = Number(match[1]);
|
|
1018
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
1019
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
1020
|
+
}
|
|
1021
|
+
function actionsEqual(actual, expected) {
|
|
1022
|
+
const actualActions = [...actual].sort();
|
|
1023
|
+
const expectedActions = [...expected].sort();
|
|
1024
|
+
return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
|
|
1025
|
+
}
|
|
1026
|
+
function hasCaveats(permission) {
|
|
1027
|
+
return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/remote-store.ts
|
|
810
1031
|
var PROFILE = "agent";
|
|
811
1032
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
1033
|
+
var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
1034
|
+
"DELEGATION_ARTIFACT_INVALID",
|
|
1035
|
+
"DELEGATION_EXPIRED",
|
|
1036
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
1037
|
+
"DELEGATION_HOST_MISMATCH",
|
|
1038
|
+
"DELEGATION_REJECTED"
|
|
1039
|
+
]);
|
|
812
1040
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
813
1041
|
var BOOTSTRAP_PERMISSIONS = [
|
|
814
1042
|
{
|
|
@@ -850,6 +1078,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
850
1078
|
this.detail = detail;
|
|
851
1079
|
}
|
|
852
1080
|
};
|
|
1081
|
+
var NativeGrantValidationError = class extends Error {
|
|
1082
|
+
code;
|
|
1083
|
+
constructor(status) {
|
|
1084
|
+
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";
|
|
1085
|
+
super(code);
|
|
1086
|
+
this.name = "NativeGrantValidationError";
|
|
1087
|
+
this.code = code;
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
853
1090
|
var RemoteTenantStore = class {
|
|
854
1091
|
#config;
|
|
855
1092
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -862,9 +1099,186 @@ var RemoteTenantStore = class {
|
|
|
862
1099
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
863
1100
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
864
1101
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
1102
|
+
#nativeIdentity;
|
|
1103
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
865
1104
|
constructor(config) {
|
|
866
1105
|
this.#config = config;
|
|
867
1106
|
}
|
|
1107
|
+
async nativeServiceIdentity() {
|
|
1108
|
+
const identity = await this.#loadNativeServiceKey();
|
|
1109
|
+
return { serviceDid: normalizeDid(identity.did) };
|
|
1110
|
+
}
|
|
1111
|
+
nativeTenantStateRoot(ownerDid) {
|
|
1112
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
1113
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
1114
|
+
const tenant = createHmac("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
|
|
1115
|
+
return join2(this.#config.stateDir, "native-tenants", tenant);
|
|
1116
|
+
}
|
|
1117
|
+
async acceptNativeGrant(serializedDelegation) {
|
|
1118
|
+
if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
|
|
1119
|
+
throw new NativeGrantValidationError("rejected");
|
|
1120
|
+
}
|
|
1121
|
+
const identity = await this.#loadNativeServiceKey();
|
|
1122
|
+
const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
|
|
1123
|
+
const { ownerDid, spaceId, permissions, expiresAt } = activated;
|
|
1124
|
+
return this.#withNativeGrantLock(ownerDid, async () => {
|
|
1125
|
+
const stateRoot = this.nativeTenantStateRoot(ownerDid);
|
|
1126
|
+
let previous = null;
|
|
1127
|
+
try {
|
|
1128
|
+
const stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
1129
|
+
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))) {
|
|
1130
|
+
previous = stored;
|
|
1131
|
+
}
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
1134
|
+
}
|
|
1135
|
+
if (previous !== null) {
|
|
1136
|
+
const previousExpiry = Date.parse(previous.expiresAt);
|
|
1137
|
+
const nextExpiry = expiresAt.getTime();
|
|
1138
|
+
if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
|
|
1139
|
+
throw new NativeGrantValidationError("rejected");
|
|
1140
|
+
}
|
|
1141
|
+
if (previous.delegationCid === activated.cid) {
|
|
1142
|
+
return {
|
|
1143
|
+
ownerDid,
|
|
1144
|
+
serviceDid: normalizeDid(identity.did),
|
|
1145
|
+
expiresAt: previous.expiresAt,
|
|
1146
|
+
stateRoot
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const session = {
|
|
1151
|
+
delegationHeader: activated.delegation.delegationHeader,
|
|
1152
|
+
delegationCid: activated.cid,
|
|
1153
|
+
spaceId,
|
|
1154
|
+
jwk: identity.jwk,
|
|
1155
|
+
verificationMethod: identity.did,
|
|
1156
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1157
|
+
};
|
|
1158
|
+
const grant = {
|
|
1159
|
+
version: 1,
|
|
1160
|
+
ownerDid,
|
|
1161
|
+
serviceDid: normalizeDid(identity.did),
|
|
1162
|
+
serializedDelegation: serializeDelegation(activated.delegation),
|
|
1163
|
+
delegationCid: activated.cid,
|
|
1164
|
+
spaceId,
|
|
1165
|
+
permissions,
|
|
1166
|
+
expiresAt: expiresAt.toISOString()
|
|
1167
|
+
};
|
|
1168
|
+
const sessionPath = this.#sessionPath(stateRoot);
|
|
1169
|
+
const previousSession = await readOptionalText(sessionPath);
|
|
1170
|
+
try {
|
|
1171
|
+
await secureWrite(this.#profilePath(stateRoot), {
|
|
1172
|
+
name: PROFILE,
|
|
1173
|
+
host: this.#config.nodeHost,
|
|
1174
|
+
chainId: activated.delegation.chainId,
|
|
1175
|
+
spaceName: "applications",
|
|
1176
|
+
did: identity.did,
|
|
1177
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1178
|
+
profile: PROFILE,
|
|
1179
|
+
operatorType: "agent",
|
|
1180
|
+
posture: "delegate-session",
|
|
1181
|
+
ownerDid,
|
|
1182
|
+
sessionDid: identity.did,
|
|
1183
|
+
spaceId,
|
|
1184
|
+
tinycloudHost: this.#config.nodeHost
|
|
1185
|
+
});
|
|
1186
|
+
await secureWrite(this.#keyPath(stateRoot), identity.jwk);
|
|
1187
|
+
await secureWrite(sessionPath, session);
|
|
1188
|
+
const request = await withTinyCloudStateRoot(stateRoot, () => createOrReusePermissionRequest({
|
|
1189
|
+
profile: PROFILE,
|
|
1190
|
+
posture: "delegate-session",
|
|
1191
|
+
operatorType: "agent",
|
|
1192
|
+
host: this.#config.nodeHost,
|
|
1193
|
+
sessionDid: identity.did,
|
|
1194
|
+
ownerDid,
|
|
1195
|
+
spaceId,
|
|
1196
|
+
missing: permissions,
|
|
1197
|
+
granted: [],
|
|
1198
|
+
replace: true
|
|
1199
|
+
}));
|
|
1200
|
+
if (request.status !== "created") {
|
|
1201
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1202
|
+
}
|
|
1203
|
+
const imported = await invokeOperation2(
|
|
1204
|
+
"tinycloud.auth.import",
|
|
1205
|
+
1,
|
|
1206
|
+
{ profile: PROFILE, stateRoot },
|
|
1207
|
+
{
|
|
1208
|
+
kind: "tinycloud.auth.delegation",
|
|
1209
|
+
version: 1,
|
|
1210
|
+
requestId: request.request.requestId,
|
|
1211
|
+
delegationCid: activated.cid,
|
|
1212
|
+
delegation: activated.delegation
|
|
1213
|
+
}
|
|
1214
|
+
);
|
|
1215
|
+
if (imported.status === "error") {
|
|
1216
|
+
throw new NativeGrantValidationError(
|
|
1217
|
+
NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
|
|
1221
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1222
|
+
}
|
|
1223
|
+
await secureWrite(this.#nativeGrantPath(stateRoot), grant);
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
try {
|
|
1226
|
+
await restoreOptionalText(sessionPath, previousSession);
|
|
1227
|
+
} catch {
|
|
1228
|
+
await rm(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
|
|
1229
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1230
|
+
}
|
|
1231
|
+
throw error;
|
|
1232
|
+
}
|
|
1233
|
+
return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
async nativeGrantForOwner(ownerDid) {
|
|
1237
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
1238
|
+
if (canonical === void 0) return { status: "rejected" };
|
|
1239
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
1240
|
+
let stored;
|
|
1241
|
+
try {
|
|
1242
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
1243
|
+
} catch {
|
|
1244
|
+
return { status: "rejected" };
|
|
1245
|
+
}
|
|
1246
|
+
if (stored === null) return { status: "required" };
|
|
1247
|
+
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) {
|
|
1248
|
+
return { status: "rejected" };
|
|
1249
|
+
}
|
|
1250
|
+
const expires = Date.parse(stored.expiresAt);
|
|
1251
|
+
if (!Number.isFinite(expires)) return { status: "rejected" };
|
|
1252
|
+
if (expires <= Date.now()) return { status: "expired" };
|
|
1253
|
+
try {
|
|
1254
|
+
const identity = await this.#loadNativeServiceKey();
|
|
1255
|
+
if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
|
|
1256
|
+
const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
|
|
1257
|
+
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" };
|
|
1258
|
+
return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (error instanceof NativeGrantValidationError) {
|
|
1261
|
+
return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
|
|
1262
|
+
}
|
|
1263
|
+
return { status: "unavailable" };
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
async assertNativeGrantActive(ownerDid) {
|
|
1267
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
1268
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
1269
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
1270
|
+
let stored;
|
|
1271
|
+
try {
|
|
1272
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
1273
|
+
} catch {
|
|
1274
|
+
throw new NativeGrantValidationError("rejected");
|
|
1275
|
+
}
|
|
1276
|
+
if (stored === null) throw new NativeGrantValidationError("required");
|
|
1277
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
1278
|
+
throw new NativeGrantValidationError("rejected");
|
|
1279
|
+
}
|
|
1280
|
+
if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
|
|
1281
|
+
}
|
|
868
1282
|
tenantStateRoot(subject) {
|
|
869
1283
|
return join2(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
870
1284
|
}
|
|
@@ -1416,7 +1830,7 @@ var RemoteTenantStore = class {
|
|
|
1416
1830
|
const expected = this.#signState(tenantId, nonce);
|
|
1417
1831
|
const supplied = Buffer.from(signature, "base64url");
|
|
1418
1832
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
1419
|
-
if (supplied.length !== expectedBytes.length || !
|
|
1833
|
+
if (supplied.length !== expectedBytes.length || !timingSafeEqual2(supplied, expectedBytes)) {
|
|
1420
1834
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1421
1835
|
}
|
|
1422
1836
|
const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -1467,6 +1881,123 @@ var RemoteTenantStore = class {
|
|
|
1467
1881
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
1468
1882
|
return url.toString();
|
|
1469
1883
|
}
|
|
1884
|
+
async #withNativeGrantLock(ownerDid, action) {
|
|
1885
|
+
const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
|
|
1886
|
+
let releaseNext;
|
|
1887
|
+
const next = new Promise((resolve) => {
|
|
1888
|
+
releaseNext = resolve;
|
|
1889
|
+
});
|
|
1890
|
+
this.#nativeGrantQueues.set(ownerDid, next);
|
|
1891
|
+
await previous;
|
|
1892
|
+
try {
|
|
1893
|
+
return await action();
|
|
1894
|
+
} finally {
|
|
1895
|
+
releaseNext();
|
|
1896
|
+
if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
async #loadNativeServiceKey() {
|
|
1900
|
+
if (this.#nativeIdentity === void 0) {
|
|
1901
|
+
const loading = this.#readOrCreateNativeServiceKey();
|
|
1902
|
+
this.#nativeIdentity = loading;
|
|
1903
|
+
void loading.catch(() => {
|
|
1904
|
+
if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
return this.#nativeIdentity;
|
|
1908
|
+
}
|
|
1909
|
+
async #readOrCreateNativeServiceKey() {
|
|
1910
|
+
const path = join2(this.#config.stateDir, ".tinycloud", "native-service-key.json");
|
|
1911
|
+
await secureDirectory(dirname(path));
|
|
1912
|
+
let stored = await readJson(path);
|
|
1913
|
+
if (stored === null) {
|
|
1914
|
+
const generated = generateSessionKey();
|
|
1915
|
+
try {
|
|
1916
|
+
await writeFile(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
|
|
1917
|
+
`, {
|
|
1918
|
+
mode: 384,
|
|
1919
|
+
flag: "wx"
|
|
1920
|
+
});
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
if (!isRecord(error) || error.code !== "EEXIST") throw error;
|
|
1923
|
+
}
|
|
1924
|
+
await chmod(path, 384);
|
|
1925
|
+
stored = await readJson(path);
|
|
1926
|
+
}
|
|
1927
|
+
if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
|
|
1928
|
+
throw new Error("The native MCP service identity is unavailable.");
|
|
1929
|
+
}
|
|
1930
|
+
return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
|
|
1931
|
+
}
|
|
1932
|
+
async #activateNativeDelegation(serialized, identity) {
|
|
1933
|
+
let delegation;
|
|
1934
|
+
try {
|
|
1935
|
+
const parsed = JSON.parse(serialized);
|
|
1936
|
+
if (!isRecord(parsed)) throw new Error("invalid delegation");
|
|
1937
|
+
delegation = deserializeDelegation(serialized);
|
|
1938
|
+
} catch {
|
|
1939
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1940
|
+
}
|
|
1941
|
+
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())) {
|
|
1942
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1943
|
+
}
|
|
1944
|
+
const now = Date.now();
|
|
1945
|
+
const claimedExpiry = delegation.expiry.getTime();
|
|
1946
|
+
if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
|
|
1947
|
+
if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1948
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1949
|
+
}
|
|
1950
|
+
let ownerDid;
|
|
1951
|
+
let spaceId;
|
|
1952
|
+
const node = new TinyCloudNode({ host: this.#config.nodeHost });
|
|
1953
|
+
try {
|
|
1954
|
+
await node.restoreSession({
|
|
1955
|
+
delegationHeader: delegation.delegationHeader,
|
|
1956
|
+
delegationCid: delegation.cid,
|
|
1957
|
+
spaceId: delegation.spaceId,
|
|
1958
|
+
jwk: identity.jwk,
|
|
1959
|
+
verificationMethod: identity.did,
|
|
1960
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1961
|
+
});
|
|
1962
|
+
const activated = await activateValidatedRuntimeDelegation(node, delegation, {
|
|
1963
|
+
host: this.#config.nodeHost,
|
|
1964
|
+
authorize: (permissions) => {
|
|
1965
|
+
const derivedOwner = nativeOwnerForConnectorGrant(permissions);
|
|
1966
|
+
const signedSpace = permissions[0]?.space;
|
|
1967
|
+
if (derivedOwner === void 0 || signedSpace === void 0) return false;
|
|
1968
|
+
const metadataOwner = canonicalOwnerDid(
|
|
1969
|
+
`did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
|
|
1970
|
+
);
|
|
1971
|
+
if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
|
|
1972
|
+
ownerDid = derivedOwner;
|
|
1973
|
+
spaceId = signedSpace;
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1978
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1979
|
+
}
|
|
1980
|
+
return {
|
|
1981
|
+
ownerDid,
|
|
1982
|
+
spaceId,
|
|
1983
|
+
permissions: [...activated.effectivePermissions],
|
|
1984
|
+
cid: activated.cid,
|
|
1985
|
+
expiresAt: activated.expiry,
|
|
1986
|
+
delegation: activated.delegation
|
|
1987
|
+
};
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
if (error instanceof NativeGrantValidationError) throw error;
|
|
1990
|
+
if (error instanceof RuntimeDelegationRejectedError) {
|
|
1991
|
+
throw new NativeGrantValidationError(
|
|
1992
|
+
error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
#nativeGrantPath(stateRoot) {
|
|
1999
|
+
return join2(stateRoot, ".tinycloud", "native-grant.json");
|
|
2000
|
+
}
|
|
1470
2001
|
#tenantId(subject) {
|
|
1471
2002
|
return createHmac("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
1472
2003
|
}
|
|
@@ -1560,6 +2091,21 @@ function samePrincipal(left, right) {
|
|
|
1560
2091
|
function normalizeDid(value) {
|
|
1561
2092
|
return value.split("#", 1)[0];
|
|
1562
2093
|
}
|
|
2094
|
+
function sameNodeHost(left, right) {
|
|
2095
|
+
try {
|
|
2096
|
+
const normalize = (value) => {
|
|
2097
|
+
const url = new URL(value);
|
|
2098
|
+
if (url.username !== "" || url.password !== "") return "";
|
|
2099
|
+
url.hash = "";
|
|
2100
|
+
url.search = "";
|
|
2101
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
2102
|
+
return url.toString().replace(/\/+$/, "");
|
|
2103
|
+
};
|
|
2104
|
+
return normalize(left) !== "" && normalize(left) === normalize(right);
|
|
2105
|
+
} catch {
|
|
2106
|
+
return false;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
1563
2109
|
function isPermissionEntry(value) {
|
|
1564
2110
|
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");
|
|
1565
2111
|
}
|
|
@@ -1590,6 +2136,23 @@ async function readJson(path) {
|
|
|
1590
2136
|
throw error;
|
|
1591
2137
|
}
|
|
1592
2138
|
}
|
|
2139
|
+
async function readOptionalText(path) {
|
|
2140
|
+
try {
|
|
2141
|
+
return await readFile(path, "utf8");
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
2144
|
+
throw error;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
async function restoreOptionalText(path, contents) {
|
|
2148
|
+
if (contents === null) {
|
|
2149
|
+
await rm(path, { force: true });
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
await secureDirectory(dirname(path));
|
|
2153
|
+
await writeFile(path, contents, { mode: 384 });
|
|
2154
|
+
await chmod(path, 384);
|
|
2155
|
+
}
|
|
1593
2156
|
async function reusablePending(directory, expected) {
|
|
1594
2157
|
let names;
|
|
1595
2158
|
try {
|
|
@@ -1622,15 +2185,22 @@ function samePendingRequest(pending, expected) {
|
|
|
1622
2185
|
|
|
1623
2186
|
// src/http.ts
|
|
1624
2187
|
var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
2188
|
+
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
2189
|
+
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1625
2190
|
function createHostedMcpApp(config) {
|
|
2191
|
+
if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
|
|
2192
|
+
throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
|
|
2193
|
+
}
|
|
1626
2194
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1627
2195
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1628
|
-
const
|
|
2196
|
+
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
2197
|
+
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1629
2198
|
const plugin = createHostedSurface({
|
|
1630
2199
|
...config,
|
|
2200
|
+
nativeServiceCredential: void 0,
|
|
1631
2201
|
stateDir: join3(config.stateDir, "chatgpt"),
|
|
1632
2202
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1633
|
-
}, pluginUrl, "public");
|
|
2203
|
+
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
1634
2204
|
const pluginPaths = {
|
|
1635
2205
|
[pluginUrl.pathname]: true,
|
|
1636
2206
|
[new URL("./connect", pluginUrl).pathname]: true,
|
|
@@ -1644,7 +2214,7 @@ function createHostedMcpApp(config) {
|
|
|
1644
2214
|
}
|
|
1645
2215
|
};
|
|
1646
2216
|
}
|
|
1647
|
-
function createHostedSurface(config, resourceUrl, surface) {
|
|
2217
|
+
function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
1648
2218
|
const connectPath = config.approvalPath ?? "/connect";
|
|
1649
2219
|
const callbackPath = `${connectPath}/callback`;
|
|
1650
2220
|
const issuer = String(config.oauthMetadata.issuer);
|
|
@@ -1653,7 +2223,8 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1653
2223
|
issuer,
|
|
1654
2224
|
audience: resourceUrl.toString(),
|
|
1655
2225
|
jwksUri,
|
|
1656
|
-
requiredScope: REQUIRED_SCOPE
|
|
2226
|
+
requiredScope: REQUIRED_SCOPE,
|
|
2227
|
+
...tokenStatus === void 0 ? {} : { tokenStatus }
|
|
1657
2228
|
});
|
|
1658
2229
|
const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(resourceUrl);
|
|
1659
2230
|
const authenticate = requireBearerAuth({
|
|
@@ -1662,16 +2233,19 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1662
2233
|
resourceMetadataUrl
|
|
1663
2234
|
});
|
|
1664
2235
|
const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
|
|
2236
|
+
const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
|
|
2237
|
+
if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
|
|
1665
2238
|
const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
|
|
1666
|
-
const
|
|
2239
|
+
const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
|
|
2240
|
+
const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
|
|
1667
2241
|
const { subject, ownerDids } = identity;
|
|
1668
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2242
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1669
2243
|
const validator = createJsonSchemaValidator();
|
|
1670
2244
|
const server = new McpServer2(
|
|
1671
2245
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1672
2246
|
{
|
|
1673
2247
|
jsonSchemaValidator: validator,
|
|
1674
|
-
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."
|
|
2248
|
+
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."
|
|
1675
2249
|
}
|
|
1676
2250
|
);
|
|
1677
2251
|
registerTinyCloudTools(server, {
|
|
@@ -1680,10 +2254,15 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1680
2254
|
explicitProfile: true,
|
|
1681
2255
|
allowOwnerProfile: false,
|
|
1682
2256
|
stateRoot,
|
|
1683
|
-
|
|
1684
|
-
|
|
2257
|
+
...nativeOwner === void 0 ? {} : {
|
|
2258
|
+
allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
|
|
2259
|
+
},
|
|
2260
|
+
beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
|
|
2261
|
+
...nativeOwner === void 0 ? {
|
|
2262
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
2263
|
+
} : {}
|
|
1685
2264
|
}, validator);
|
|
1686
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2265
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1687
2266
|
return server;
|
|
1688
2267
|
}, {
|
|
1689
2268
|
legacy: "stateless",
|
|
@@ -1699,14 +2278,205 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1699
2278
|
const allowedHosts = [resourceUrl.hostname];
|
|
1700
2279
|
return {
|
|
1701
2280
|
async fetch(request) {
|
|
1702
|
-
const metadata = oauthMetadataResponse(request, metadataOptions);
|
|
1703
|
-
if (metadata !== void 0) return metadata;
|
|
1704
2281
|
const url = new URL(request.url);
|
|
2282
|
+
if (url.pathname !== "/healthz") {
|
|
2283
|
+
const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
|
|
2284
|
+
if (hostFailure !== void 0) return hostFailure;
|
|
2285
|
+
}
|
|
2286
|
+
if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
|
|
2287
|
+
const metadataUrl = new URL(getOAuthProtectedResourceMetadataUrl(resourceUrl));
|
|
2288
|
+
const metadataRequest = new Request(metadataUrl.toString(), {
|
|
2289
|
+
method: request.method,
|
|
2290
|
+
headers: request.headers
|
|
2291
|
+
});
|
|
2292
|
+
const response = oauthMetadataResponse(metadataRequest, metadataOptions);
|
|
2293
|
+
if (response !== void 0) return withBearerMethodsSupported(response);
|
|
2294
|
+
}
|
|
2295
|
+
const metadata = oauthMetadataResponse(request, metadataOptions);
|
|
2296
|
+
if (metadata !== void 0) {
|
|
2297
|
+
return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
|
|
2298
|
+
}
|
|
2299
|
+
if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
|
|
2300
|
+
if (request.method === "OPTIONS") {
|
|
2301
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2302
|
+
}
|
|
2303
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2304
|
+
return publicMetadataCors(Response.json({
|
|
2305
|
+
specVersion: "1.0",
|
|
2306
|
+
host: {
|
|
2307
|
+
displayName: "TinyCloud, Inc.",
|
|
2308
|
+
identifier: "https://mcp.tinycloud.xyz",
|
|
2309
|
+
documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
|
|
2310
|
+
},
|
|
2311
|
+
entries: [{
|
|
2312
|
+
identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
|
|
2313
|
+
displayName: "TinyCloud MCP",
|
|
2314
|
+
type: "application/json",
|
|
2315
|
+
description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
|
|
2316
|
+
url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
|
|
2317
|
+
representativeQueries: [
|
|
2318
|
+
"How do I connect to TinyCloud's hosted MCP server?",
|
|
2319
|
+
"Which endpoint provides TinyCloud's delegated MCP operations?",
|
|
2320
|
+
"How can I connect to TinyCloud MCP using local stdio?"
|
|
2321
|
+
]
|
|
2322
|
+
}]
|
|
2323
|
+
}, {
|
|
2324
|
+
headers: {
|
|
2325
|
+
"cache-control": "public, max-age=300",
|
|
2326
|
+
"content-type": "application/json; charset=utf-8"
|
|
2327
|
+
}
|
|
2328
|
+
}), request);
|
|
2329
|
+
}
|
|
2330
|
+
if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
|
|
2331
|
+
if (request.method === "OPTIONS") {
|
|
2332
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2333
|
+
}
|
|
2334
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2335
|
+
return publicMetadataCors(Response.json({
|
|
2336
|
+
protocol_version: "0.1",
|
|
2337
|
+
organization: {
|
|
2338
|
+
name: "TinyCloud, Inc.",
|
|
2339
|
+
domain: resourceUrl.hostname
|
|
2340
|
+
},
|
|
2341
|
+
web: {}
|
|
2342
|
+
}, {
|
|
2343
|
+
headers: {
|
|
2344
|
+
"cache-control": "public, max-age=300",
|
|
2345
|
+
"content-type": "application/json; charset=utf-8"
|
|
2346
|
+
}
|
|
2347
|
+
}), request);
|
|
2348
|
+
}
|
|
2349
|
+
if (url.pathname === "/auth.md" && surface === "hosted") {
|
|
2350
|
+
if (request.method === "OPTIONS") {
|
|
2351
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2352
|
+
}
|
|
2353
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2354
|
+
const issuerUrl = new URL(issuer);
|
|
2355
|
+
const authorizationServerMetadataUrl = new URL(
|
|
2356
|
+
`/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
|
|
2357
|
+
issuerUrl.origin
|
|
2358
|
+
);
|
|
2359
|
+
const registrationEndpoint = config.oauthMetadata.registration_endpoint;
|
|
2360
|
+
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.";
|
|
2361
|
+
const authDoc = [
|
|
2362
|
+
"# TinyCloud MCP auth.md",
|
|
2363
|
+
"",
|
|
2364
|
+
`Protected resource: ${resourceUrl}.`,
|
|
2365
|
+
"",
|
|
2366
|
+
"Send an OAuth access token in the `Authorization: Bearer` header.",
|
|
2367
|
+
"",
|
|
2368
|
+
`Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
|
|
2369
|
+
`Authorization server issuer: ${issuer}.`,
|
|
2370
|
+
`Authorization server metadata: ${authorizationServerMetadataUrl}.`,
|
|
2371
|
+
`Scope: \`${REQUIRED_SCOPE}\`.`,
|
|
2372
|
+
"",
|
|
2373
|
+
"Hosted access uses OpenKey OAuth.",
|
|
2374
|
+
registrationNote,
|
|
2375
|
+
"TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
|
|
2376
|
+
"No `agent_auth` registration block is published because no agent-registration endpoint is available.",
|
|
2377
|
+
"",
|
|
2378
|
+
`Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
|
|
2379
|
+
"",
|
|
2380
|
+
"Connection steps: https://docs.tinycloud.xyz/cli/mcp"
|
|
2381
|
+
].join("\n") + "\n";
|
|
2382
|
+
return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
|
|
2383
|
+
}
|
|
2384
|
+
if (url.pathname === "/robots.txt" && surface === "hosted") {
|
|
2385
|
+
return request.method === "GET" ? publicText(`User-agent: *
|
|
2386
|
+
Allow: /
|
|
2387
|
+
Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
|
|
2388
|
+
Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
2389
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2390
|
+
}
|
|
2391
|
+
if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
|
|
2392
|
+
if (request.method === "OPTIONS") {
|
|
2393
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2394
|
+
}
|
|
2395
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2396
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2397
|
+
const card = {
|
|
2398
|
+
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
2399
|
+
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
2400
|
+
title: "TinyCloud MCP",
|
|
2401
|
+
version: MCP_VERSION,
|
|
2402
|
+
description: "MCP server for delegated TinyCloud operations.",
|
|
2403
|
+
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
2404
|
+
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
2405
|
+
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
2406
|
+
};
|
|
2407
|
+
if (identity !== void 0) {
|
|
2408
|
+
card["x-tinycloud-native-auth"] = {
|
|
2409
|
+
serviceDid: identity.serviceDid,
|
|
2410
|
+
serviceDidField: "x-tinycloud-native-auth.serviceDid",
|
|
2411
|
+
grantEndpoint: {
|
|
2412
|
+
method: "POST",
|
|
2413
|
+
path: "/native/grant",
|
|
2414
|
+
body: { delegation: "serialized PortableDelegation JSON string" }
|
|
2415
|
+
},
|
|
2416
|
+
serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
|
|
2417
|
+
serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
|
|
2418
|
+
ownerDidHeader: NATIVE_OWNER_DID_HEADER,
|
|
2419
|
+
maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
|
|
2420
|
+
acceptedCapabilities: [
|
|
2421
|
+
{ service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
|
|
2422
|
+
{ service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
|
|
2423
|
+
],
|
|
2424
|
+
tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
|
|
2425
|
+
errors: {
|
|
2426
|
+
required: NATIVE_GRANT_REQUIRED,
|
|
2427
|
+
expired: NATIVE_GRANT_EXPIRED,
|
|
2428
|
+
rejected: NATIVE_GRANT_REJECTED,
|
|
2429
|
+
serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
|
|
2430
|
+
serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
|
|
2431
|
+
validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
|
|
2432
|
+
}
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
return publicMetadataCors(Response.json(card, {
|
|
2436
|
+
headers: { "cache-control": "public, max-age=300" }
|
|
2437
|
+
}), request);
|
|
2438
|
+
}
|
|
2439
|
+
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
2440
|
+
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
2441
|
+
|
|
2442
|
+
Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
|
|
2443
|
+
|
|
2444
|
+
Connection documentation: https://docs.tinycloud.xyz/cli/mcp
|
|
2445
|
+
|
|
2446
|
+
Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
|
|
2447
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2448
|
+
}
|
|
2449
|
+
if (url.pathname === "/sitemap.xml" && surface === "hosted") {
|
|
2450
|
+
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>`;
|
|
2451
|
+
return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
|
|
2452
|
+
}
|
|
2453
|
+
if (url.pathname === "/" && surface === "hosted") {
|
|
2454
|
+
if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
|
|
2455
|
+
const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
|
|
2456
|
+
const content = markdown ? `# TinyCloud MCP
|
|
2457
|
+
|
|
2458
|
+
Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
|
|
2459
|
+
|
|
2460
|
+
[Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
|
|
2461
|
+
|
|
2462
|
+
Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
|
|
2463
|
+
` : `<!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>`;
|
|
2464
|
+
const headers = new Headers({
|
|
2465
|
+
"content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
|
|
2466
|
+
"cache-control": "public, max-age=300",
|
|
2467
|
+
"content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
2468
|
+
"x-content-type-options": "nosniff",
|
|
2469
|
+
vary: "Accept"
|
|
2470
|
+
});
|
|
2471
|
+
headers.set(
|
|
2472
|
+
"link",
|
|
2473
|
+
`<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
|
|
2474
|
+
);
|
|
2475
|
+
return new Response(request.method === "HEAD" ? null : content, { headers });
|
|
2476
|
+
}
|
|
1705
2477
|
if (url.pathname === "/healthz") {
|
|
1706
2478
|
return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
|
|
1707
2479
|
}
|
|
1708
|
-
const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
|
|
1709
|
-
if (hostFailure !== void 0) return hostFailure;
|
|
1710
2480
|
if (url.pathname === connectPath) {
|
|
1711
2481
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1712
2482
|
try {
|
|
@@ -1762,6 +2532,73 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1762
2532
|
);
|
|
1763
2533
|
}
|
|
1764
2534
|
}
|
|
2535
|
+
if (url.pathname === "/native/grant" && surface === "hosted") {
|
|
2536
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
2537
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2538
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2539
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2540
|
+
}
|
|
2541
|
+
if (!serviceCredentialMatches(
|
|
2542
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2543
|
+
config.nativeServiceCredential
|
|
2544
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2545
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
2546
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2547
|
+
}
|
|
2548
|
+
const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
|
|
2549
|
+
if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2550
|
+
let body;
|
|
2551
|
+
try {
|
|
2552
|
+
body = JSON.parse(bodyText);
|
|
2553
|
+
} catch {
|
|
2554
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2555
|
+
}
|
|
2556
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
|
|
2557
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2558
|
+
}
|
|
2559
|
+
const grantBody = body;
|
|
2560
|
+
if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
|
|
2561
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2562
|
+
}
|
|
2563
|
+
try {
|
|
2564
|
+
const accepted = await store.acceptNativeGrant(grantBody.delegation);
|
|
2565
|
+
return json({
|
|
2566
|
+
ownerDid: accepted.ownerDid,
|
|
2567
|
+
serviceDid: accepted.serviceDid,
|
|
2568
|
+
expiresAt: accepted.expiresAt
|
|
2569
|
+
}, 201);
|
|
2570
|
+
} catch (error) {
|
|
2571
|
+
return nativeGrantErrorResponse(error);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
|
|
2575
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2576
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2577
|
+
}
|
|
2578
|
+
if (!serviceCredentialMatches(
|
|
2579
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2580
|
+
config.nativeServiceCredential
|
|
2581
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2582
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2583
|
+
const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
|
|
2584
|
+
if (ownerHeader === null || ownerHeader.length === 0) {
|
|
2585
|
+
return json({ error: NATIVE_GRANT_REQUIRED }, 401);
|
|
2586
|
+
}
|
|
2587
|
+
const grant = await store.nativeGrantForOwner(ownerHeader);
|
|
2588
|
+
if (grant.status !== "ok") {
|
|
2589
|
+
const status = grant.status === "unavailable" ? 503 : 401;
|
|
2590
|
+
const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
|
|
2591
|
+
return json({ error: code }, status);
|
|
2592
|
+
}
|
|
2593
|
+
return handler.fetch(request, {
|
|
2594
|
+
authInfo: {
|
|
2595
|
+
token: "native-service",
|
|
2596
|
+
clientId: "tinycloud-native",
|
|
2597
|
+
scopes: [],
|
|
2598
|
+
extra: { nativeOwnerDid: grant.ownerDid }
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
1765
2602
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
1766
2603
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
1767
2604
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -1873,9 +2710,77 @@ function callbackCors(response, openkeyHost) {
|
|
|
1873
2710
|
headers.set("vary", "Origin");
|
|
1874
2711
|
return new Response(response.body, { status: response.status, headers });
|
|
1875
2712
|
}
|
|
2713
|
+
async function readBoundedBodyText(request, maxBytes) {
|
|
2714
|
+
const contentLength = request.headers.get("content-length");
|
|
2715
|
+
if (contentLength !== null) {
|
|
2716
|
+
const parsedLength = Number(contentLength);
|
|
2717
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
|
|
2718
|
+
}
|
|
2719
|
+
if (request.body === null) return "";
|
|
2720
|
+
const reader = request.body.getReader();
|
|
2721
|
+
const chunks = [];
|
|
2722
|
+
let total = 0;
|
|
2723
|
+
try {
|
|
2724
|
+
while (true) {
|
|
2725
|
+
const { done, value } = await reader.read();
|
|
2726
|
+
if (done) break;
|
|
2727
|
+
total += value.byteLength;
|
|
2728
|
+
if (total > maxBytes) {
|
|
2729
|
+
await reader.cancel();
|
|
2730
|
+
return void 0;
|
|
2731
|
+
}
|
|
2732
|
+
chunks.push(value);
|
|
2733
|
+
}
|
|
2734
|
+
} finally {
|
|
2735
|
+
reader.releaseLock();
|
|
2736
|
+
}
|
|
2737
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2738
|
+
}
|
|
2739
|
+
function nativeGrantErrorResponse(error) {
|
|
2740
|
+
if (!(error instanceof NativeGrantValidationError)) {
|
|
2741
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2742
|
+
}
|
|
2743
|
+
const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
|
|
2744
|
+
return json({ error: error.code }, unavailable ? 503 : 401);
|
|
2745
|
+
}
|
|
1876
2746
|
function json(value, status = 200) {
|
|
1877
2747
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
1878
2748
|
}
|
|
2749
|
+
async function withBearerMethodsSupported(response) {
|
|
2750
|
+
if (response.status === 204 || response.body === null) return response;
|
|
2751
|
+
const metadata = await response.json();
|
|
2752
|
+
const headers = new Headers(response.headers);
|
|
2753
|
+
headers.delete("content-length");
|
|
2754
|
+
headers.delete("content-encoding");
|
|
2755
|
+
return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
|
|
2756
|
+
status: response.status,
|
|
2757
|
+
statusText: response.statusText,
|
|
2758
|
+
headers
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
function publicMetadataCors(response, request) {
|
|
2762
|
+
const headers = new Headers(response.headers);
|
|
2763
|
+
headers.set("access-control-allow-origin", "*");
|
|
2764
|
+
headers.set("access-control-allow-methods", "GET, OPTIONS");
|
|
2765
|
+
const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
|
|
2766
|
+
if (requestedHeaders !== null) {
|
|
2767
|
+
headers.set("access-control-allow-headers", requestedHeaders);
|
|
2768
|
+
}
|
|
2769
|
+
headers.set(
|
|
2770
|
+
"vary",
|
|
2771
|
+
requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
|
|
2772
|
+
);
|
|
2773
|
+
return new Response(response.body, { status: response.status, headers });
|
|
2774
|
+
}
|
|
2775
|
+
function publicText(text, contentType) {
|
|
2776
|
+
return new Response(text, {
|
|
2777
|
+
headers: {
|
|
2778
|
+
"content-type": contentType,
|
|
2779
|
+
"cache-control": "public, max-age=300",
|
|
2780
|
+
"x-content-type-options": "nosniff"
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
1879
2784
|
function methodNotAllowed(allow) {
|
|
1880
2785
|
return new Response("Method not allowed", { status: 405, headers: { allow } });
|
|
1881
2786
|
}
|