@tinycloud/mcp 0.8.3-beta.2 → 0.8.3-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -44
- package/dist/cli.cjs +4 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +940 -26
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +939 -22
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +927 -25
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +24 -0
- package/dist/http.d.ts +24 -0
- package/dist/http.js +926 -21
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +927 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +926 -21
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +4 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +3 -1
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +3 -3
- package/skills/tinycloud-delegated-secrets/SKILL.md +18 -4
package/dist/index.cjs
CHANGED
|
@@ -444,7 +444,7 @@ function createJsonSchemaValidator() {
|
|
|
444
444
|
return new import_ajv.AjvJsonSchemaValidator();
|
|
445
445
|
}
|
|
446
446
|
function registerTinyCloudTools(server, selection, validator) {
|
|
447
|
-
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
|
|
447
|
+
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
|
|
448
448
|
for (const binding of bindings) {
|
|
449
449
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
450
450
|
if (operation === void 0) {
|
|
@@ -522,7 +522,7 @@ function outcomeCode(result) {
|
|
|
522
522
|
// package.json
|
|
523
523
|
var package_default = {
|
|
524
524
|
name: "@tinycloud/mcp",
|
|
525
|
-
version: "0.8.3-beta.
|
|
525
|
+
version: "0.8.3-beta.20",
|
|
526
526
|
description: "TinyCloud delegated operations MCP server",
|
|
527
527
|
keywords: [
|
|
528
528
|
"tinycloud",
|
|
@@ -593,9 +593,9 @@ var package_default = {
|
|
|
593
593
|
dependencies: {
|
|
594
594
|
"@modelcontextprotocol/node": "2.0.0-beta.5",
|
|
595
595
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
596
|
-
"@tinycloud/node-sdk": "3.8.0-beta.
|
|
596
|
+
"@tinycloud/node-sdk": "3.8.0-beta.18",
|
|
597
597
|
"@tinycloud/node-sdk-wasm": "1.7.8",
|
|
598
|
-
"@tinycloud/operations": "0.7.7-beta.
|
|
598
|
+
"@tinycloud/operations": "0.7.7-beta.17",
|
|
599
599
|
jose: "6.2.4"
|
|
600
600
|
},
|
|
601
601
|
devDependencies: {
|
|
@@ -752,7 +752,9 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
752
752
|
issuer: config.issuer,
|
|
753
753
|
audience: config.audience
|
|
754
754
|
});
|
|
755
|
-
|
|
755
|
+
const info = authInfo(token, payload, config);
|
|
756
|
+
if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
|
|
757
|
+
return info;
|
|
756
758
|
} catch (error) {
|
|
757
759
|
if (error instanceof import_jose.errors.JOSEError) {
|
|
758
760
|
throw new import_server4.OAuthError(import_server4.OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
@@ -762,6 +764,16 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
762
764
|
}
|
|
763
765
|
};
|
|
764
766
|
}
|
|
767
|
+
async function requireUsableStatus(checker, token) {
|
|
768
|
+
const status = await checker.tokenStatus(token);
|
|
769
|
+
if (status === "active") return;
|
|
770
|
+
if (status === "inactive") {
|
|
771
|
+
throw new import_server4.OAuthError(import_server4.OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
|
|
772
|
+
}
|
|
773
|
+
if (!checker.toleratesUnknown(token)) {
|
|
774
|
+
throw new import_server4.OAuthError(import_server4.OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
765
777
|
function authInfo(token, payload, config) {
|
|
766
778
|
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
767
779
|
throw new import_server4.OAuthError(import_server4.OAuthErrorCode.InvalidToken, "The OpenKey access token does not identify a subject.");
|
|
@@ -815,17 +827,230 @@ function firstString(...values) {
|
|
|
815
827
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
816
828
|
}
|
|
817
829
|
|
|
818
|
-
// src/
|
|
830
|
+
// src/token-status.ts
|
|
819
831
|
var import_node_crypto = require("crypto");
|
|
832
|
+
var import_jose2 = require("jose");
|
|
833
|
+
var ACTIVE_CACHE_MS = 3e4;
|
|
834
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
835
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
836
|
+
var MAX_CACHED_TOKENS = 1e4;
|
|
837
|
+
function createTokenStatusChecker(config) {
|
|
838
|
+
const doFetch = config.fetch ?? fetch;
|
|
839
|
+
const clock = config.now ?? Date.now;
|
|
840
|
+
const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
841
|
+
`));
|
|
842
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
843
|
+
const inactive = /* @__PURE__ */ new Map();
|
|
844
|
+
const active = /* @__PURE__ */ new Map();
|
|
845
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
846
|
+
const confirmedActive = /* @__PURE__ */ new Map();
|
|
847
|
+
let credentialsRefused = false;
|
|
848
|
+
async function introspect(token) {
|
|
849
|
+
const headers = {
|
|
850
|
+
accept: "application/json",
|
|
851
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
852
|
+
};
|
|
853
|
+
const body = new URLSearchParams({ token, token_type_hint: "access_token" });
|
|
854
|
+
if (config.authMethod === "basic") {
|
|
855
|
+
const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
856
|
+
const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
|
|
857
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
858
|
+
} else {
|
|
859
|
+
body.set("client_id", config.clientId);
|
|
860
|
+
body.set("client_secret", config.clientSecret);
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const response = await doFetch(config.endpoint, {
|
|
864
|
+
method: "POST",
|
|
865
|
+
headers,
|
|
866
|
+
body,
|
|
867
|
+
redirect: "error",
|
|
868
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
869
|
+
});
|
|
870
|
+
if (response.status === 401) return "credentials";
|
|
871
|
+
if (response.status !== 200) return "unknown";
|
|
872
|
+
const result = await response.json();
|
|
873
|
+
if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
|
|
874
|
+
if (result.active === false) return "inactive";
|
|
875
|
+
if (result.active !== true) return "unknown";
|
|
876
|
+
return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
|
|
877
|
+
} catch {
|
|
878
|
+
return "unknown";
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function query(key, token) {
|
|
882
|
+
const outcome = await introspect(token);
|
|
883
|
+
if (outcome === "credentials") {
|
|
884
|
+
if (!credentialsRefused) {
|
|
885
|
+
log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
|
|
886
|
+
}
|
|
887
|
+
credentialsRefused = true;
|
|
888
|
+
return "unknown";
|
|
889
|
+
}
|
|
890
|
+
if (outcome === "unknown") return outcome;
|
|
891
|
+
const now = clock();
|
|
892
|
+
prune(now);
|
|
893
|
+
if (outcome === "mismatch") {
|
|
894
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
895
|
+
confirmedActive.delete(key);
|
|
896
|
+
return "inactive";
|
|
897
|
+
}
|
|
898
|
+
credentialsRefused = false;
|
|
899
|
+
if (outcome === "inactive") {
|
|
900
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
901
|
+
confirmedActive.delete(key);
|
|
902
|
+
} else {
|
|
903
|
+
active.set(key, now + ACTIVE_CACHE_MS);
|
|
904
|
+
confirmedActive.delete(key);
|
|
905
|
+
confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
|
|
906
|
+
}
|
|
907
|
+
return outcome;
|
|
908
|
+
}
|
|
909
|
+
function prune(now) {
|
|
910
|
+
for (const [key, until] of active) if (until <= now) active.delete(key);
|
|
911
|
+
for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
|
|
912
|
+
for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
|
|
913
|
+
for (const cache of [active, inactive, confirmedActive]) {
|
|
914
|
+
while (cache.size >= MAX_CACHED_TOKENS) {
|
|
915
|
+
const oldest = cache.keys().next().value;
|
|
916
|
+
if (oldest === void 0) break;
|
|
917
|
+
cache.delete(oldest);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
async tokenStatus(token) {
|
|
923
|
+
const key = (0, import_node_crypto.createHash)("sha256").update(token).digest("hex");
|
|
924
|
+
const now = clock();
|
|
925
|
+
const inactiveUntil = inactive.get(key);
|
|
926
|
+
if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
|
|
927
|
+
const activeUntil = active.get(key);
|
|
928
|
+
if (activeUntil !== void 0 && activeUntil > now) return "active";
|
|
929
|
+
const pending = inFlight.get(key);
|
|
930
|
+
if (pending !== void 0) return pending;
|
|
931
|
+
const asking = query(key, token).finally(() => inFlight.delete(key));
|
|
932
|
+
inFlight.set(key, asking);
|
|
933
|
+
return asking;
|
|
934
|
+
},
|
|
935
|
+
toleratesUnknown(token) {
|
|
936
|
+
if (credentialsRefused) return false;
|
|
937
|
+
const confirmed = confirmedActive.get((0, import_node_crypto.createHash)("sha256").update(token).digest("hex"));
|
|
938
|
+
if (confirmed === void 0) return false;
|
|
939
|
+
const now = clock();
|
|
940
|
+
return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function answerMatchesToken(answer, token, resourceUrl) {
|
|
945
|
+
const jwt = claims(token);
|
|
946
|
+
if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
|
|
947
|
+
const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
|
|
948
|
+
if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
|
|
949
|
+
if (jwt.aud === void 0) return true;
|
|
950
|
+
const answered = audienceList(answer.aud);
|
|
951
|
+
const signed = audienceList(jwt.aud);
|
|
952
|
+
return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
|
|
953
|
+
}
|
|
954
|
+
function audienceList(value) {
|
|
955
|
+
const list = typeof value === "string" ? [value] : value;
|
|
956
|
+
return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
|
|
957
|
+
}
|
|
958
|
+
function tokenExpiryMs(token, now) {
|
|
959
|
+
const exp = claims(token).exp;
|
|
960
|
+
return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
|
|
961
|
+
}
|
|
962
|
+
function tokenLifetimeSeconds(token) {
|
|
963
|
+
const { exp, iat } = claims(token);
|
|
964
|
+
return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
965
|
+
}
|
|
966
|
+
function claims(token) {
|
|
967
|
+
try {
|
|
968
|
+
return (0, import_jose2.decodeJwt)(token);
|
|
969
|
+
} catch {
|
|
970
|
+
return {};
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/remote-store.ts
|
|
975
|
+
var import_node_crypto3 = require("crypto");
|
|
820
976
|
var import_promises = require("fs/promises");
|
|
821
977
|
var import_node_path3 = require("path");
|
|
822
978
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
823
979
|
var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
|
|
980
|
+
var import_artifacts = require("@tinycloud/operations/artifacts");
|
|
981
|
+
var import_state = require("@tinycloud/operations/state");
|
|
824
982
|
var import_operations2 = require("@tinycloud/operations");
|
|
825
983
|
var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
|
|
826
984
|
var import_tc_apps = require("@tinycloud/operations/tc-apps");
|
|
985
|
+
|
|
986
|
+
// src/native-auth.ts
|
|
987
|
+
var import_node_crypto2 = require("crypto");
|
|
988
|
+
var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
|
|
989
|
+
var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
|
|
990
|
+
var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
991
|
+
var NATIVE_GRANT_REQUIRED = "native_grant_required";
|
|
992
|
+
var NATIVE_GRANT_EXPIRED = "native_grant_expired";
|
|
993
|
+
var NATIVE_GRANT_REJECTED = "native_grant_rejected";
|
|
994
|
+
var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
|
|
995
|
+
var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
|
|
996
|
+
var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
|
|
997
|
+
var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
|
|
998
|
+
function serviceCredentialMatches(supplied, configured) {
|
|
999
|
+
if (supplied === null || configured === void 0) return false;
|
|
1000
|
+
const suppliedBytes = Buffer.from(supplied, "utf8");
|
|
1001
|
+
const configuredBytes = Buffer.from(configured, "utf8");
|
|
1002
|
+
return suppliedBytes.byteLength === configuredBytes.byteLength && (0, import_node_crypto2.timingSafeEqual)(suppliedBytes, configuredBytes);
|
|
1003
|
+
}
|
|
1004
|
+
var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
|
|
1005
|
+
var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
|
|
1006
|
+
var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
|
|
1007
|
+
function canonicalOwnerDid(value) {
|
|
1008
|
+
const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
|
|
1009
|
+
if (!match) return void 0;
|
|
1010
|
+
const chainId = Number(match[1]);
|
|
1011
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
1012
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
1013
|
+
}
|
|
1014
|
+
function nativeOwnerForConnectorGrant(permissions) {
|
|
1015
|
+
if (permissions.length !== 2) return void 0;
|
|
1016
|
+
const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
|
|
1017
|
+
const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
|
|
1018
|
+
if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
|
|
1019
|
+
const sql = sqlEntries[0];
|
|
1020
|
+
const kv = kvEntries[0];
|
|
1021
|
+
if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
|
|
1022
|
+
if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
|
|
1023
|
+
if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
|
|
1024
|
+
const sqlSpace = ownerFromApplicationsSpace(sql.space);
|
|
1025
|
+
const kvSpace = ownerFromApplicationsSpace(kv.space);
|
|
1026
|
+
return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
|
|
1027
|
+
}
|
|
1028
|
+
function ownerFromApplicationsSpace(space) {
|
|
1029
|
+
const match = OWNER_APPLICATIONS_SPACE.exec(space);
|
|
1030
|
+
if (!match) return void 0;
|
|
1031
|
+
const chainId = Number(match[1]);
|
|
1032
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
1033
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
1034
|
+
}
|
|
1035
|
+
function actionsEqual(actual, expected) {
|
|
1036
|
+
const actualActions = [...actual].sort();
|
|
1037
|
+
const expectedActions = [...expected].sort();
|
|
1038
|
+
return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
|
|
1039
|
+
}
|
|
1040
|
+
function hasCaveats(permission) {
|
|
1041
|
+
return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/remote-store.ts
|
|
827
1045
|
var PROFILE = "agent";
|
|
828
1046
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
1047
|
+
var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
1048
|
+
"DELEGATION_ARTIFACT_INVALID",
|
|
1049
|
+
"DELEGATION_EXPIRED",
|
|
1050
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
1051
|
+
"DELEGATION_HOST_MISMATCH",
|
|
1052
|
+
"DELEGATION_REJECTED"
|
|
1053
|
+
]);
|
|
829
1054
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
830
1055
|
var BOOTSTRAP_PERMISSIONS = [
|
|
831
1056
|
{
|
|
@@ -867,6 +1092,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
867
1092
|
this.detail = detail;
|
|
868
1093
|
}
|
|
869
1094
|
};
|
|
1095
|
+
var NativeGrantValidationError = class extends Error {
|
|
1096
|
+
code;
|
|
1097
|
+
constructor(status) {
|
|
1098
|
+
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";
|
|
1099
|
+
super(code);
|
|
1100
|
+
this.name = "NativeGrantValidationError";
|
|
1101
|
+
this.code = code;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
870
1104
|
var RemoteTenantStore = class {
|
|
871
1105
|
#config;
|
|
872
1106
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -879,9 +1113,186 @@ var RemoteTenantStore = class {
|
|
|
879
1113
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
880
1114
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
881
1115
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
1116
|
+
#nativeIdentity;
|
|
1117
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
882
1118
|
constructor(config) {
|
|
883
1119
|
this.#config = config;
|
|
884
1120
|
}
|
|
1121
|
+
async nativeServiceIdentity() {
|
|
1122
|
+
const identity = await this.#loadNativeServiceKey();
|
|
1123
|
+
return { serviceDid: normalizeDid(identity.did) };
|
|
1124
|
+
}
|
|
1125
|
+
nativeTenantStateRoot(ownerDid) {
|
|
1126
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
1127
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
1128
|
+
const tenant = (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
|
|
1129
|
+
return (0, import_node_path3.join)(this.#config.stateDir, "native-tenants", tenant);
|
|
1130
|
+
}
|
|
1131
|
+
async acceptNativeGrant(serializedDelegation) {
|
|
1132
|
+
if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
|
|
1133
|
+
throw new NativeGrantValidationError("rejected");
|
|
1134
|
+
}
|
|
1135
|
+
const identity = await this.#loadNativeServiceKey();
|
|
1136
|
+
const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
|
|
1137
|
+
const { ownerDid, spaceId, permissions, expiresAt } = activated;
|
|
1138
|
+
return this.#withNativeGrantLock(ownerDid, async () => {
|
|
1139
|
+
const stateRoot = this.nativeTenantStateRoot(ownerDid);
|
|
1140
|
+
let previous = null;
|
|
1141
|
+
try {
|
|
1142
|
+
const stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
1143
|
+
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))) {
|
|
1144
|
+
previous = stored;
|
|
1145
|
+
}
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
1148
|
+
}
|
|
1149
|
+
if (previous !== null) {
|
|
1150
|
+
const previousExpiry = Date.parse(previous.expiresAt);
|
|
1151
|
+
const nextExpiry = expiresAt.getTime();
|
|
1152
|
+
if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
|
|
1153
|
+
throw new NativeGrantValidationError("rejected");
|
|
1154
|
+
}
|
|
1155
|
+
if (previous.delegationCid === activated.cid) {
|
|
1156
|
+
return {
|
|
1157
|
+
ownerDid,
|
|
1158
|
+
serviceDid: normalizeDid(identity.did),
|
|
1159
|
+
expiresAt: previous.expiresAt,
|
|
1160
|
+
stateRoot
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const session = {
|
|
1165
|
+
delegationHeader: activated.delegation.delegationHeader,
|
|
1166
|
+
delegationCid: activated.cid,
|
|
1167
|
+
spaceId,
|
|
1168
|
+
jwk: identity.jwk,
|
|
1169
|
+
verificationMethod: identity.did,
|
|
1170
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1171
|
+
};
|
|
1172
|
+
const grant = {
|
|
1173
|
+
version: 1,
|
|
1174
|
+
ownerDid,
|
|
1175
|
+
serviceDid: normalizeDid(identity.did),
|
|
1176
|
+
serializedDelegation: (0, import_node_sdk2.serializeDelegation)(activated.delegation),
|
|
1177
|
+
delegationCid: activated.cid,
|
|
1178
|
+
spaceId,
|
|
1179
|
+
permissions,
|
|
1180
|
+
expiresAt: expiresAt.toISOString()
|
|
1181
|
+
};
|
|
1182
|
+
const sessionPath = this.#sessionPath(stateRoot);
|
|
1183
|
+
const previousSession = await readOptionalText(sessionPath);
|
|
1184
|
+
try {
|
|
1185
|
+
await secureWrite(this.#profilePath(stateRoot), {
|
|
1186
|
+
name: PROFILE,
|
|
1187
|
+
host: this.#config.nodeHost,
|
|
1188
|
+
chainId: activated.delegation.chainId,
|
|
1189
|
+
spaceName: "applications",
|
|
1190
|
+
did: identity.did,
|
|
1191
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1192
|
+
profile: PROFILE,
|
|
1193
|
+
operatorType: "agent",
|
|
1194
|
+
posture: "delegate-session",
|
|
1195
|
+
ownerDid,
|
|
1196
|
+
sessionDid: identity.did,
|
|
1197
|
+
spaceId,
|
|
1198
|
+
tinycloudHost: this.#config.nodeHost
|
|
1199
|
+
});
|
|
1200
|
+
await secureWrite(this.#keyPath(stateRoot), identity.jwk);
|
|
1201
|
+
await secureWrite(sessionPath, session);
|
|
1202
|
+
const request = await (0, import_state.withTinyCloudStateRoot)(stateRoot, () => (0, import_artifacts.createOrReusePermissionRequest)({
|
|
1203
|
+
profile: PROFILE,
|
|
1204
|
+
posture: "delegate-session",
|
|
1205
|
+
operatorType: "agent",
|
|
1206
|
+
host: this.#config.nodeHost,
|
|
1207
|
+
sessionDid: identity.did,
|
|
1208
|
+
ownerDid,
|
|
1209
|
+
spaceId,
|
|
1210
|
+
missing: permissions,
|
|
1211
|
+
granted: [],
|
|
1212
|
+
replace: true
|
|
1213
|
+
}));
|
|
1214
|
+
if (request.status !== "created") {
|
|
1215
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1216
|
+
}
|
|
1217
|
+
const imported = await (0, import_operations2.invokeOperation)(
|
|
1218
|
+
"tinycloud.auth.import",
|
|
1219
|
+
1,
|
|
1220
|
+
{ profile: PROFILE, stateRoot },
|
|
1221
|
+
{
|
|
1222
|
+
kind: "tinycloud.auth.delegation",
|
|
1223
|
+
version: 1,
|
|
1224
|
+
requestId: request.request.requestId,
|
|
1225
|
+
delegationCid: activated.cid,
|
|
1226
|
+
delegation: activated.delegation
|
|
1227
|
+
}
|
|
1228
|
+
);
|
|
1229
|
+
if (imported.status === "error") {
|
|
1230
|
+
throw new NativeGrantValidationError(
|
|
1231
|
+
NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
|
|
1235
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1236
|
+
}
|
|
1237
|
+
await secureWrite(this.#nativeGrantPath(stateRoot), grant);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
try {
|
|
1240
|
+
await restoreOptionalText(sessionPath, previousSession);
|
|
1241
|
+
} catch {
|
|
1242
|
+
await (0, import_promises.rm)(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
|
|
1243
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1244
|
+
}
|
|
1245
|
+
throw error;
|
|
1246
|
+
}
|
|
1247
|
+
return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
async nativeGrantForOwner(ownerDid) {
|
|
1251
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
1252
|
+
if (canonical === void 0) return { status: "rejected" };
|
|
1253
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
1254
|
+
let stored;
|
|
1255
|
+
try {
|
|
1256
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
1257
|
+
} catch {
|
|
1258
|
+
return { status: "rejected" };
|
|
1259
|
+
}
|
|
1260
|
+
if (stored === null) return { status: "required" };
|
|
1261
|
+
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) {
|
|
1262
|
+
return { status: "rejected" };
|
|
1263
|
+
}
|
|
1264
|
+
const expires = Date.parse(stored.expiresAt);
|
|
1265
|
+
if (!Number.isFinite(expires)) return { status: "rejected" };
|
|
1266
|
+
if (expires <= Date.now()) return { status: "expired" };
|
|
1267
|
+
try {
|
|
1268
|
+
const identity = await this.#loadNativeServiceKey();
|
|
1269
|
+
if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
|
|
1270
|
+
const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
|
|
1271
|
+
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" };
|
|
1272
|
+
return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
if (error instanceof NativeGrantValidationError) {
|
|
1275
|
+
return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
|
|
1276
|
+
}
|
|
1277
|
+
return { status: "unavailable" };
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async assertNativeGrantActive(ownerDid) {
|
|
1281
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
1282
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
1283
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
1284
|
+
let stored;
|
|
1285
|
+
try {
|
|
1286
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
1287
|
+
} catch {
|
|
1288
|
+
throw new NativeGrantValidationError("rejected");
|
|
1289
|
+
}
|
|
1290
|
+
if (stored === null) throw new NativeGrantValidationError("required");
|
|
1291
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
1292
|
+
throw new NativeGrantValidationError("rejected");
|
|
1293
|
+
}
|
|
1294
|
+
if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
|
|
1295
|
+
}
|
|
885
1296
|
tenantStateRoot(subject) {
|
|
886
1297
|
return (0, import_node_path3.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
887
1298
|
}
|
|
@@ -1408,7 +1819,7 @@ var RemoteTenantStore = class {
|
|
|
1408
1819
|
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
1409
1820
|
);
|
|
1410
1821
|
if (reusable !== void 0) return reusable;
|
|
1411
|
-
const nonce = (0,
|
|
1822
|
+
const nonce = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
|
|
1412
1823
|
const created = Date.now();
|
|
1413
1824
|
const pending = {
|
|
1414
1825
|
version: 1,
|
|
@@ -1433,7 +1844,7 @@ var RemoteTenantStore = class {
|
|
|
1433
1844
|
const expected = this.#signState(tenantId, nonce);
|
|
1434
1845
|
const supplied = Buffer.from(signature, "base64url");
|
|
1435
1846
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
1436
|
-
if (supplied.length !== expectedBytes.length || !(0,
|
|
1847
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto3.timingSafeEqual)(supplied, expectedBytes)) {
|
|
1437
1848
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1438
1849
|
}
|
|
1439
1850
|
const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -1484,11 +1895,128 @@ var RemoteTenantStore = class {
|
|
|
1484
1895
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
1485
1896
|
return url.toString();
|
|
1486
1897
|
}
|
|
1898
|
+
async #withNativeGrantLock(ownerDid, action) {
|
|
1899
|
+
const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
|
|
1900
|
+
let releaseNext;
|
|
1901
|
+
const next = new Promise((resolve) => {
|
|
1902
|
+
releaseNext = resolve;
|
|
1903
|
+
});
|
|
1904
|
+
this.#nativeGrantQueues.set(ownerDid, next);
|
|
1905
|
+
await previous;
|
|
1906
|
+
try {
|
|
1907
|
+
return await action();
|
|
1908
|
+
} finally {
|
|
1909
|
+
releaseNext();
|
|
1910
|
+
if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
async #loadNativeServiceKey() {
|
|
1914
|
+
if (this.#nativeIdentity === void 0) {
|
|
1915
|
+
const loading = this.#readOrCreateNativeServiceKey();
|
|
1916
|
+
this.#nativeIdentity = loading;
|
|
1917
|
+
void loading.catch(() => {
|
|
1918
|
+
if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
return this.#nativeIdentity;
|
|
1922
|
+
}
|
|
1923
|
+
async #readOrCreateNativeServiceKey() {
|
|
1924
|
+
const path = (0, import_node_path3.join)(this.#config.stateDir, ".tinycloud", "native-service-key.json");
|
|
1925
|
+
await secureDirectory((0, import_node_path3.dirname)(path));
|
|
1926
|
+
let stored = await readJson(path);
|
|
1927
|
+
if (stored === null) {
|
|
1928
|
+
const generated = generateSessionKey();
|
|
1929
|
+
try {
|
|
1930
|
+
await (0, import_promises.writeFile)(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
|
|
1931
|
+
`, {
|
|
1932
|
+
mode: 384,
|
|
1933
|
+
flag: "wx"
|
|
1934
|
+
});
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
if (!isRecord(error) || error.code !== "EEXIST") throw error;
|
|
1937
|
+
}
|
|
1938
|
+
await (0, import_promises.chmod)(path, 384);
|
|
1939
|
+
stored = await readJson(path);
|
|
1940
|
+
}
|
|
1941
|
+
if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
|
|
1942
|
+
throw new Error("The native MCP service identity is unavailable.");
|
|
1943
|
+
}
|
|
1944
|
+
return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
|
|
1945
|
+
}
|
|
1946
|
+
async #activateNativeDelegation(serialized, identity) {
|
|
1947
|
+
let delegation;
|
|
1948
|
+
try {
|
|
1949
|
+
const parsed = JSON.parse(serialized);
|
|
1950
|
+
if (!isRecord(parsed)) throw new Error("invalid delegation");
|
|
1951
|
+
delegation = (0, import_node_sdk2.deserializeDelegation)(serialized);
|
|
1952
|
+
} catch {
|
|
1953
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1954
|
+
}
|
|
1955
|
+
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())) {
|
|
1956
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1957
|
+
}
|
|
1958
|
+
const now = Date.now();
|
|
1959
|
+
const claimedExpiry = delegation.expiry.getTime();
|
|
1960
|
+
if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
|
|
1961
|
+
if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1962
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1963
|
+
}
|
|
1964
|
+
let ownerDid;
|
|
1965
|
+
let spaceId;
|
|
1966
|
+
const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
|
|
1967
|
+
try {
|
|
1968
|
+
await node.restoreSession({
|
|
1969
|
+
delegationHeader: delegation.delegationHeader,
|
|
1970
|
+
delegationCid: delegation.cid,
|
|
1971
|
+
spaceId: delegation.spaceId,
|
|
1972
|
+
jwk: identity.jwk,
|
|
1973
|
+
verificationMethod: identity.did,
|
|
1974
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1975
|
+
});
|
|
1976
|
+
const activated = await (0, import_node_sdk2.activateValidatedRuntimeDelegation)(node, delegation, {
|
|
1977
|
+
host: this.#config.nodeHost,
|
|
1978
|
+
authorize: (permissions) => {
|
|
1979
|
+
const derivedOwner = nativeOwnerForConnectorGrant(permissions);
|
|
1980
|
+
const signedSpace = permissions[0]?.space;
|
|
1981
|
+
if (derivedOwner === void 0 || signedSpace === void 0) return false;
|
|
1982
|
+
const metadataOwner = canonicalOwnerDid(
|
|
1983
|
+
`did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
|
|
1984
|
+
);
|
|
1985
|
+
if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
|
|
1986
|
+
ownerDid = derivedOwner;
|
|
1987
|
+
spaceId = signedSpace;
|
|
1988
|
+
return true;
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1992
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1993
|
+
}
|
|
1994
|
+
return {
|
|
1995
|
+
ownerDid,
|
|
1996
|
+
spaceId,
|
|
1997
|
+
permissions: [...activated.effectivePermissions],
|
|
1998
|
+
cid: activated.cid,
|
|
1999
|
+
expiresAt: activated.expiry,
|
|
2000
|
+
delegation: activated.delegation
|
|
2001
|
+
};
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
if (error instanceof NativeGrantValidationError) throw error;
|
|
2004
|
+
if (error instanceof import_node_sdk2.RuntimeDelegationRejectedError) {
|
|
2005
|
+
throw new NativeGrantValidationError(
|
|
2006
|
+
error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
#nativeGrantPath(stateRoot) {
|
|
2013
|
+
return (0, import_node_path3.join)(stateRoot, ".tinycloud", "native-grant.json");
|
|
2014
|
+
}
|
|
1487
2015
|
#tenantId(subject) {
|
|
1488
|
-
return (0,
|
|
2016
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
1489
2017
|
}
|
|
1490
2018
|
#signState(tenantId, nonce) {
|
|
1491
|
-
return (0,
|
|
2019
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
1492
2020
|
}
|
|
1493
2021
|
#profilePath(stateRoot) {
|
|
1494
2022
|
return (0, import_node_path3.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
@@ -1577,6 +2105,21 @@ function samePrincipal(left, right) {
|
|
|
1577
2105
|
function normalizeDid(value) {
|
|
1578
2106
|
return value.split("#", 1)[0];
|
|
1579
2107
|
}
|
|
2108
|
+
function sameNodeHost(left, right) {
|
|
2109
|
+
try {
|
|
2110
|
+
const normalize = (value) => {
|
|
2111
|
+
const url = new URL(value);
|
|
2112
|
+
if (url.username !== "" || url.password !== "") return "";
|
|
2113
|
+
url.hash = "";
|
|
2114
|
+
url.search = "";
|
|
2115
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
2116
|
+
return url.toString().replace(/\/+$/, "");
|
|
2117
|
+
};
|
|
2118
|
+
return normalize(left) !== "" && normalize(left) === normalize(right);
|
|
2119
|
+
} catch {
|
|
2120
|
+
return false;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
1580
2123
|
function isPermissionEntry(value) {
|
|
1581
2124
|
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");
|
|
1582
2125
|
}
|
|
@@ -1590,7 +2133,7 @@ async function secureDirectory(path) {
|
|
|
1590
2133
|
async function secureWrite(path, value) {
|
|
1591
2134
|
const directory = (0, import_node_path3.dirname)(path);
|
|
1592
2135
|
await secureDirectory(directory);
|
|
1593
|
-
const temporary = `${path}.${process.pid}.${(0,
|
|
2136
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`;
|
|
1594
2137
|
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
1595
2138
|
`, { mode: 384 });
|
|
1596
2139
|
await (0, import_promises.rename)(temporary, path);
|
|
@@ -1607,6 +2150,23 @@ async function readJson(path) {
|
|
|
1607
2150
|
throw error;
|
|
1608
2151
|
}
|
|
1609
2152
|
}
|
|
2153
|
+
async function readOptionalText(path) {
|
|
2154
|
+
try {
|
|
2155
|
+
return await (0, import_promises.readFile)(path, "utf8");
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
2158
|
+
throw error;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
async function restoreOptionalText(path, contents) {
|
|
2162
|
+
if (contents === null) {
|
|
2163
|
+
await (0, import_promises.rm)(path, { force: true });
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
await secureDirectory((0, import_node_path3.dirname)(path));
|
|
2167
|
+
await (0, import_promises.writeFile)(path, contents, { mode: 384 });
|
|
2168
|
+
await (0, import_promises.chmod)(path, 384);
|
|
2169
|
+
}
|
|
1610
2170
|
async function reusablePending(directory, expected) {
|
|
1611
2171
|
let names;
|
|
1612
2172
|
try {
|
|
@@ -1639,15 +2199,22 @@ function samePendingRequest(pending, expected) {
|
|
|
1639
2199
|
|
|
1640
2200
|
// src/http.ts
|
|
1641
2201
|
var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
2202
|
+
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
2203
|
+
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1642
2204
|
function createHostedMcpApp(config) {
|
|
2205
|
+
if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
|
|
2206
|
+
throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
|
|
2207
|
+
}
|
|
1643
2208
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1644
2209
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1645
|
-
const
|
|
2210
|
+
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
2211
|
+
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1646
2212
|
const plugin = createHostedSurface({
|
|
1647
2213
|
...config,
|
|
2214
|
+
nativeServiceCredential: void 0,
|
|
1648
2215
|
stateDir: (0, import_node_path4.join)(config.stateDir, "chatgpt"),
|
|
1649
2216
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1650
|
-
}, pluginUrl, "public");
|
|
2217
|
+
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
1651
2218
|
const pluginPaths = {
|
|
1652
2219
|
[pluginUrl.pathname]: true,
|
|
1653
2220
|
[new URL("./connect", pluginUrl).pathname]: true,
|
|
@@ -1661,7 +2228,7 @@ function createHostedMcpApp(config) {
|
|
|
1661
2228
|
}
|
|
1662
2229
|
};
|
|
1663
2230
|
}
|
|
1664
|
-
function createHostedSurface(config, resourceUrl, surface) {
|
|
2231
|
+
function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
1665
2232
|
const connectPath = config.approvalPath ?? "/connect";
|
|
1666
2233
|
const callbackPath = `${connectPath}/callback`;
|
|
1667
2234
|
const issuer = String(config.oauthMetadata.issuer);
|
|
@@ -1670,7 +2237,8 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1670
2237
|
issuer,
|
|
1671
2238
|
audience: resourceUrl.toString(),
|
|
1672
2239
|
jwksUri,
|
|
1673
|
-
requiredScope: REQUIRED_SCOPE
|
|
2240
|
+
requiredScope: REQUIRED_SCOPE,
|
|
2241
|
+
...tokenStatus === void 0 ? {} : { tokenStatus }
|
|
1674
2242
|
});
|
|
1675
2243
|
const resourceMetadataUrl = (0, import_server5.getOAuthProtectedResourceMetadataUrl)(resourceUrl);
|
|
1676
2244
|
const authenticate = (0, import_server5.requireBearerAuth)({
|
|
@@ -1679,16 +2247,19 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1679
2247
|
resourceMetadataUrl
|
|
1680
2248
|
});
|
|
1681
2249
|
const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
|
|
2250
|
+
const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
|
|
2251
|
+
if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
|
|
1682
2252
|
const handler = (0, import_server5.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
|
|
1683
|
-
const
|
|
2253
|
+
const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
|
|
2254
|
+
const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
|
|
1684
2255
|
const { subject, ownerDids } = identity;
|
|
1685
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2256
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1686
2257
|
const validator = createJsonSchemaValidator();
|
|
1687
2258
|
const server = new import_server5.McpServer(
|
|
1688
2259
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1689
2260
|
{
|
|
1690
2261
|
jsonSchemaValidator: validator,
|
|
1691
|
-
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."
|
|
2262
|
+
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."
|
|
1692
2263
|
}
|
|
1693
2264
|
);
|
|
1694
2265
|
registerTinyCloudTools(server, {
|
|
@@ -1697,10 +2268,15 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1697
2268
|
explicitProfile: true,
|
|
1698
2269
|
allowOwnerProfile: false,
|
|
1699
2270
|
stateRoot,
|
|
1700
|
-
|
|
1701
|
-
|
|
2271
|
+
...nativeOwner === void 0 ? {} : {
|
|
2272
|
+
allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
|
|
2273
|
+
},
|
|
2274
|
+
beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
|
|
2275
|
+
...nativeOwner === void 0 ? {
|
|
2276
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
2277
|
+
} : {}
|
|
1702
2278
|
}, validator);
|
|
1703
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2279
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1704
2280
|
return server;
|
|
1705
2281
|
}, {
|
|
1706
2282
|
legacy: "stateless",
|
|
@@ -1716,14 +2292,205 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1716
2292
|
const allowedHosts = [resourceUrl.hostname];
|
|
1717
2293
|
return {
|
|
1718
2294
|
async fetch(request) {
|
|
1719
|
-
const metadata = (0, import_server5.oauthMetadataResponse)(request, metadataOptions);
|
|
1720
|
-
if (metadata !== void 0) return metadata;
|
|
1721
2295
|
const url = new URL(request.url);
|
|
2296
|
+
if (url.pathname !== "/healthz") {
|
|
2297
|
+
const hostFailure = (0, import_server5.hostHeaderValidationResponse)(request, allowedHosts);
|
|
2298
|
+
if (hostFailure !== void 0) return hostFailure;
|
|
2299
|
+
}
|
|
2300
|
+
if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
|
|
2301
|
+
const metadataUrl = new URL((0, import_server5.getOAuthProtectedResourceMetadataUrl)(resourceUrl));
|
|
2302
|
+
const metadataRequest = new Request(metadataUrl.toString(), {
|
|
2303
|
+
method: request.method,
|
|
2304
|
+
headers: request.headers
|
|
2305
|
+
});
|
|
2306
|
+
const response = (0, import_server5.oauthMetadataResponse)(metadataRequest, metadataOptions);
|
|
2307
|
+
if (response !== void 0) return withBearerMethodsSupported(response);
|
|
2308
|
+
}
|
|
2309
|
+
const metadata = (0, import_server5.oauthMetadataResponse)(request, metadataOptions);
|
|
2310
|
+
if (metadata !== void 0) {
|
|
2311
|
+
return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
|
|
2312
|
+
}
|
|
2313
|
+
if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
|
|
2314
|
+
if (request.method === "OPTIONS") {
|
|
2315
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2316
|
+
}
|
|
2317
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2318
|
+
return publicMetadataCors(Response.json({
|
|
2319
|
+
specVersion: "1.0",
|
|
2320
|
+
host: {
|
|
2321
|
+
displayName: "TinyCloud, Inc.",
|
|
2322
|
+
identifier: "https://mcp.tinycloud.xyz",
|
|
2323
|
+
documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
|
|
2324
|
+
},
|
|
2325
|
+
entries: [{
|
|
2326
|
+
identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
|
|
2327
|
+
displayName: "TinyCloud MCP",
|
|
2328
|
+
type: "application/json",
|
|
2329
|
+
description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
|
|
2330
|
+
url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
|
|
2331
|
+
representativeQueries: [
|
|
2332
|
+
"How do I connect to TinyCloud's hosted MCP server?",
|
|
2333
|
+
"Which endpoint provides TinyCloud's delegated MCP operations?",
|
|
2334
|
+
"How can I connect to TinyCloud MCP using local stdio?"
|
|
2335
|
+
]
|
|
2336
|
+
}]
|
|
2337
|
+
}, {
|
|
2338
|
+
headers: {
|
|
2339
|
+
"cache-control": "public, max-age=300",
|
|
2340
|
+
"content-type": "application/json; charset=utf-8"
|
|
2341
|
+
}
|
|
2342
|
+
}), request);
|
|
2343
|
+
}
|
|
2344
|
+
if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
|
|
2345
|
+
if (request.method === "OPTIONS") {
|
|
2346
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2347
|
+
}
|
|
2348
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2349
|
+
return publicMetadataCors(Response.json({
|
|
2350
|
+
protocol_version: "0.1",
|
|
2351
|
+
organization: {
|
|
2352
|
+
name: "TinyCloud, Inc.",
|
|
2353
|
+
domain: resourceUrl.hostname
|
|
2354
|
+
},
|
|
2355
|
+
web: {}
|
|
2356
|
+
}, {
|
|
2357
|
+
headers: {
|
|
2358
|
+
"cache-control": "public, max-age=300",
|
|
2359
|
+
"content-type": "application/json; charset=utf-8"
|
|
2360
|
+
}
|
|
2361
|
+
}), request);
|
|
2362
|
+
}
|
|
2363
|
+
if (url.pathname === "/auth.md" && surface === "hosted") {
|
|
2364
|
+
if (request.method === "OPTIONS") {
|
|
2365
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2366
|
+
}
|
|
2367
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2368
|
+
const issuerUrl = new URL(issuer);
|
|
2369
|
+
const authorizationServerMetadataUrl = new URL(
|
|
2370
|
+
`/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
|
|
2371
|
+
issuerUrl.origin
|
|
2372
|
+
);
|
|
2373
|
+
const registrationEndpoint = config.oauthMetadata.registration_endpoint;
|
|
2374
|
+
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.";
|
|
2375
|
+
const authDoc = [
|
|
2376
|
+
"# TinyCloud MCP auth.md",
|
|
2377
|
+
"",
|
|
2378
|
+
`Protected resource: ${resourceUrl}.`,
|
|
2379
|
+
"",
|
|
2380
|
+
"Send an OAuth access token in the `Authorization: Bearer` header.",
|
|
2381
|
+
"",
|
|
2382
|
+
`Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
|
|
2383
|
+
`Authorization server issuer: ${issuer}.`,
|
|
2384
|
+
`Authorization server metadata: ${authorizationServerMetadataUrl}.`,
|
|
2385
|
+
`Scope: \`${REQUIRED_SCOPE}\`.`,
|
|
2386
|
+
"",
|
|
2387
|
+
"Hosted access uses OpenKey OAuth.",
|
|
2388
|
+
registrationNote,
|
|
2389
|
+
"TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
|
|
2390
|
+
"No `agent_auth` registration block is published because no agent-registration endpoint is available.",
|
|
2391
|
+
"",
|
|
2392
|
+
`Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
|
|
2393
|
+
"",
|
|
2394
|
+
"Connection steps: https://docs.tinycloud.xyz/cli/mcp"
|
|
2395
|
+
].join("\n") + "\n";
|
|
2396
|
+
return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
|
|
2397
|
+
}
|
|
2398
|
+
if (url.pathname === "/robots.txt" && surface === "hosted") {
|
|
2399
|
+
return request.method === "GET" ? publicText(`User-agent: *
|
|
2400
|
+
Allow: /
|
|
2401
|
+
Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
|
|
2402
|
+
Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
2403
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2404
|
+
}
|
|
2405
|
+
if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
|
|
2406
|
+
if (request.method === "OPTIONS") {
|
|
2407
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2408
|
+
}
|
|
2409
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2410
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2411
|
+
const card = {
|
|
2412
|
+
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
2413
|
+
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
2414
|
+
title: "TinyCloud MCP",
|
|
2415
|
+
version: MCP_VERSION,
|
|
2416
|
+
description: "MCP server for delegated TinyCloud operations.",
|
|
2417
|
+
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
2418
|
+
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
2419
|
+
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
2420
|
+
};
|
|
2421
|
+
if (identity !== void 0) {
|
|
2422
|
+
card["x-tinycloud-native-auth"] = {
|
|
2423
|
+
serviceDid: identity.serviceDid,
|
|
2424
|
+
serviceDidField: "x-tinycloud-native-auth.serviceDid",
|
|
2425
|
+
grantEndpoint: {
|
|
2426
|
+
method: "POST",
|
|
2427
|
+
path: "/native/grant",
|
|
2428
|
+
body: { delegation: "serialized PortableDelegation JSON string" }
|
|
2429
|
+
},
|
|
2430
|
+
serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
|
|
2431
|
+
serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
|
|
2432
|
+
ownerDidHeader: NATIVE_OWNER_DID_HEADER,
|
|
2433
|
+
maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
|
|
2434
|
+
acceptedCapabilities: [
|
|
2435
|
+
{ service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
|
|
2436
|
+
{ service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
|
|
2437
|
+
],
|
|
2438
|
+
tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
|
|
2439
|
+
errors: {
|
|
2440
|
+
required: NATIVE_GRANT_REQUIRED,
|
|
2441
|
+
expired: NATIVE_GRANT_EXPIRED,
|
|
2442
|
+
rejected: NATIVE_GRANT_REJECTED,
|
|
2443
|
+
serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
|
|
2444
|
+
serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
|
|
2445
|
+
validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
return publicMetadataCors(Response.json(card, {
|
|
2450
|
+
headers: { "cache-control": "public, max-age=300" }
|
|
2451
|
+
}), request);
|
|
2452
|
+
}
|
|
2453
|
+
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
2454
|
+
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
2455
|
+
|
|
2456
|
+
Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
|
|
2457
|
+
|
|
2458
|
+
Connection documentation: https://docs.tinycloud.xyz/cli/mcp
|
|
2459
|
+
|
|
2460
|
+
Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
|
|
2461
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2462
|
+
}
|
|
2463
|
+
if (url.pathname === "/sitemap.xml" && surface === "hosted") {
|
|
2464
|
+
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>`;
|
|
2465
|
+
return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
|
|
2466
|
+
}
|
|
2467
|
+
if (url.pathname === "/" && surface === "hosted") {
|
|
2468
|
+
if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
|
|
2469
|
+
const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
|
|
2470
|
+
const content = markdown ? `# TinyCloud MCP
|
|
2471
|
+
|
|
2472
|
+
Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
|
|
2473
|
+
|
|
2474
|
+
[Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
|
|
2475
|
+
|
|
2476
|
+
Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
|
|
2477
|
+
` : `<!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>`;
|
|
2478
|
+
const headers = new Headers({
|
|
2479
|
+
"content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
|
|
2480
|
+
"cache-control": "public, max-age=300",
|
|
2481
|
+
"content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
2482
|
+
"x-content-type-options": "nosniff",
|
|
2483
|
+
vary: "Accept"
|
|
2484
|
+
});
|
|
2485
|
+
headers.set(
|
|
2486
|
+
"link",
|
|
2487
|
+
`<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
|
|
2488
|
+
);
|
|
2489
|
+
return new Response(request.method === "HEAD" ? null : content, { headers });
|
|
2490
|
+
}
|
|
1722
2491
|
if (url.pathname === "/healthz") {
|
|
1723
2492
|
return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
|
|
1724
2493
|
}
|
|
1725
|
-
const hostFailure = (0, import_server5.hostHeaderValidationResponse)(request, allowedHosts);
|
|
1726
|
-
if (hostFailure !== void 0) return hostFailure;
|
|
1727
2494
|
if (url.pathname === connectPath) {
|
|
1728
2495
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1729
2496
|
try {
|
|
@@ -1779,6 +2546,73 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1779
2546
|
);
|
|
1780
2547
|
}
|
|
1781
2548
|
}
|
|
2549
|
+
if (url.pathname === "/native/grant" && surface === "hosted") {
|
|
2550
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
2551
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2552
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2553
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2554
|
+
}
|
|
2555
|
+
if (!serviceCredentialMatches(
|
|
2556
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2557
|
+
config.nativeServiceCredential
|
|
2558
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2559
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
2560
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2561
|
+
}
|
|
2562
|
+
const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
|
|
2563
|
+
if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2564
|
+
let body;
|
|
2565
|
+
try {
|
|
2566
|
+
body = JSON.parse(bodyText);
|
|
2567
|
+
} catch {
|
|
2568
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2569
|
+
}
|
|
2570
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
|
|
2571
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2572
|
+
}
|
|
2573
|
+
const grantBody = body;
|
|
2574
|
+
if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
|
|
2575
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2576
|
+
}
|
|
2577
|
+
try {
|
|
2578
|
+
const accepted = await store.acceptNativeGrant(grantBody.delegation);
|
|
2579
|
+
return json({
|
|
2580
|
+
ownerDid: accepted.ownerDid,
|
|
2581
|
+
serviceDid: accepted.serviceDid,
|
|
2582
|
+
expiresAt: accepted.expiresAt
|
|
2583
|
+
}, 201);
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
return nativeGrantErrorResponse(error);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
|
|
2589
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2590
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2591
|
+
}
|
|
2592
|
+
if (!serviceCredentialMatches(
|
|
2593
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2594
|
+
config.nativeServiceCredential
|
|
2595
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2596
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2597
|
+
const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
|
|
2598
|
+
if (ownerHeader === null || ownerHeader.length === 0) {
|
|
2599
|
+
return json({ error: NATIVE_GRANT_REQUIRED }, 401);
|
|
2600
|
+
}
|
|
2601
|
+
const grant = await store.nativeGrantForOwner(ownerHeader);
|
|
2602
|
+
if (grant.status !== "ok") {
|
|
2603
|
+
const status = grant.status === "unavailable" ? 503 : 401;
|
|
2604
|
+
const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
|
|
2605
|
+
return json({ error: code }, status);
|
|
2606
|
+
}
|
|
2607
|
+
return handler.fetch(request, {
|
|
2608
|
+
authInfo: {
|
|
2609
|
+
token: "native-service",
|
|
2610
|
+
clientId: "tinycloud-native",
|
|
2611
|
+
scopes: [],
|
|
2612
|
+
extra: { nativeOwnerDid: grant.ownerDid }
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
1782
2616
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
1783
2617
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
1784
2618
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -1890,9 +2724,77 @@ function callbackCors(response, openkeyHost) {
|
|
|
1890
2724
|
headers.set("vary", "Origin");
|
|
1891
2725
|
return new Response(response.body, { status: response.status, headers });
|
|
1892
2726
|
}
|
|
2727
|
+
async function readBoundedBodyText(request, maxBytes) {
|
|
2728
|
+
const contentLength = request.headers.get("content-length");
|
|
2729
|
+
if (contentLength !== null) {
|
|
2730
|
+
const parsedLength = Number(contentLength);
|
|
2731
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
|
|
2732
|
+
}
|
|
2733
|
+
if (request.body === null) return "";
|
|
2734
|
+
const reader = request.body.getReader();
|
|
2735
|
+
const chunks = [];
|
|
2736
|
+
let total = 0;
|
|
2737
|
+
try {
|
|
2738
|
+
while (true) {
|
|
2739
|
+
const { done, value } = await reader.read();
|
|
2740
|
+
if (done) break;
|
|
2741
|
+
total += value.byteLength;
|
|
2742
|
+
if (total > maxBytes) {
|
|
2743
|
+
await reader.cancel();
|
|
2744
|
+
return void 0;
|
|
2745
|
+
}
|
|
2746
|
+
chunks.push(value);
|
|
2747
|
+
}
|
|
2748
|
+
} finally {
|
|
2749
|
+
reader.releaseLock();
|
|
2750
|
+
}
|
|
2751
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2752
|
+
}
|
|
2753
|
+
function nativeGrantErrorResponse(error) {
|
|
2754
|
+
if (!(error instanceof NativeGrantValidationError)) {
|
|
2755
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2756
|
+
}
|
|
2757
|
+
const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
|
|
2758
|
+
return json({ error: error.code }, unavailable ? 503 : 401);
|
|
2759
|
+
}
|
|
1893
2760
|
function json(value, status = 200) {
|
|
1894
2761
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
1895
2762
|
}
|
|
2763
|
+
async function withBearerMethodsSupported(response) {
|
|
2764
|
+
if (response.status === 204 || response.body === null) return response;
|
|
2765
|
+
const metadata = await response.json();
|
|
2766
|
+
const headers = new Headers(response.headers);
|
|
2767
|
+
headers.delete("content-length");
|
|
2768
|
+
headers.delete("content-encoding");
|
|
2769
|
+
return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
|
|
2770
|
+
status: response.status,
|
|
2771
|
+
statusText: response.statusText,
|
|
2772
|
+
headers
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
function publicMetadataCors(response, request) {
|
|
2776
|
+
const headers = new Headers(response.headers);
|
|
2777
|
+
headers.set("access-control-allow-origin", "*");
|
|
2778
|
+
headers.set("access-control-allow-methods", "GET, OPTIONS");
|
|
2779
|
+
const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
|
|
2780
|
+
if (requestedHeaders !== null) {
|
|
2781
|
+
headers.set("access-control-allow-headers", requestedHeaders);
|
|
2782
|
+
}
|
|
2783
|
+
headers.set(
|
|
2784
|
+
"vary",
|
|
2785
|
+
requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
|
|
2786
|
+
);
|
|
2787
|
+
return new Response(response.body, { status: response.status, headers });
|
|
2788
|
+
}
|
|
2789
|
+
function publicText(text, contentType) {
|
|
2790
|
+
return new Response(text, {
|
|
2791
|
+
headers: {
|
|
2792
|
+
"content-type": contentType,
|
|
2793
|
+
"cache-control": "public, max-age=300",
|
|
2794
|
+
"x-content-type-options": "nosniff"
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
1896
2798
|
function methodNotAllowed(allow) {
|
|
1897
2799
|
return new Response("Method not allowed", { status: 405, headers: { allow } });
|
|
1898
2800
|
}
|