@tinycloud/mcp 0.8.3-beta.16 → 0.8.3-beta.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +565 -16
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +564 -12
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +564 -16
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +2 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +563 -12
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +564 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +563 -12
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +2 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +3 -1
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/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.17",
|
|
489
489
|
description: "TinyCloud delegated operations MCP server",
|
|
490
490
|
keywords: [
|
|
491
491
|
"tinycloud",
|
|
@@ -946,12 +946,15 @@ function claims(token) {
|
|
|
946
946
|
}
|
|
947
947
|
|
|
948
948
|
// src/remote-store.ts
|
|
949
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
949
|
+
import { createHmac, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
950
950
|
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
951
951
|
import { dirname, join as join2 } from "path";
|
|
952
952
|
import {
|
|
953
|
+
activateValidatedRuntimeDelegation,
|
|
954
|
+
deserializeDelegation,
|
|
953
955
|
InvalidRestoredSessionError,
|
|
954
956
|
RuntimeDelegationRejectedError,
|
|
957
|
+
serializeDelegation,
|
|
955
958
|
TinyCloudNode,
|
|
956
959
|
UnsupportedSessionRestoreError,
|
|
957
960
|
isCapabilitySubset,
|
|
@@ -960,11 +963,80 @@ import {
|
|
|
960
963
|
signedCompactUcanExpiry
|
|
961
964
|
} from "@tinycloud/node-sdk";
|
|
962
965
|
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
966
|
+
import { createOrReusePermissionRequest } from "@tinycloud/operations/artifacts";
|
|
967
|
+
import { withTinyCloudStateRoot } from "@tinycloud/operations/state";
|
|
963
968
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
964
969
|
import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
|
|
965
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
|
|
966
1031
|
var PROFILE = "agent";
|
|
967
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
|
+
]);
|
|
968
1040
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
969
1041
|
var BOOTSTRAP_PERMISSIONS = [
|
|
970
1042
|
{
|
|
@@ -1006,6 +1078,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
1006
1078
|
this.detail = detail;
|
|
1007
1079
|
}
|
|
1008
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
|
+
};
|
|
1009
1090
|
var RemoteTenantStore = class {
|
|
1010
1091
|
#config;
|
|
1011
1092
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -1018,9 +1099,186 @@ var RemoteTenantStore = class {
|
|
|
1018
1099
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
1019
1100
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
1020
1101
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
1102
|
+
#nativeIdentity;
|
|
1103
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
1021
1104
|
constructor(config) {
|
|
1022
1105
|
this.#config = config;
|
|
1023
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
|
+
}
|
|
1024
1282
|
tenantStateRoot(subject) {
|
|
1025
1283
|
return join2(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
1026
1284
|
}
|
|
@@ -1572,7 +1830,7 @@ var RemoteTenantStore = class {
|
|
|
1572
1830
|
const expected = this.#signState(tenantId, nonce);
|
|
1573
1831
|
const supplied = Buffer.from(signature, "base64url");
|
|
1574
1832
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
1575
|
-
if (supplied.length !== expectedBytes.length || !
|
|
1833
|
+
if (supplied.length !== expectedBytes.length || !timingSafeEqual2(supplied, expectedBytes)) {
|
|
1576
1834
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1577
1835
|
}
|
|
1578
1836
|
const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -1623,6 +1881,123 @@ var RemoteTenantStore = class {
|
|
|
1623
1881
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
1624
1882
|
return url.toString();
|
|
1625
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
|
+
}
|
|
1626
2001
|
#tenantId(subject) {
|
|
1627
2002
|
return createHmac("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
1628
2003
|
}
|
|
@@ -1716,6 +2091,21 @@ function samePrincipal(left, right) {
|
|
|
1716
2091
|
function normalizeDid(value) {
|
|
1717
2092
|
return value.split("#", 1)[0];
|
|
1718
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
|
+
}
|
|
1719
2109
|
function isPermissionEntry(value) {
|
|
1720
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");
|
|
1721
2111
|
}
|
|
@@ -1746,6 +2136,23 @@ async function readJson(path) {
|
|
|
1746
2136
|
throw error;
|
|
1747
2137
|
}
|
|
1748
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
|
+
}
|
|
1749
2156
|
async function reusablePending(directory, expected) {
|
|
1750
2157
|
let names;
|
|
1751
2158
|
try {
|
|
@@ -1781,12 +2188,16 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1781
2188
|
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
1782
2189
|
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1783
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
|
+
}
|
|
1784
2194
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1785
2195
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1786
2196
|
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1787
2197
|
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1788
2198
|
const plugin = createHostedSurface({
|
|
1789
2199
|
...config,
|
|
2200
|
+
nativeServiceCredential: void 0,
|
|
1790
2201
|
stateDir: join3(config.stateDir, "chatgpt"),
|
|
1791
2202
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1792
2203
|
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
@@ -1822,16 +2233,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1822
2233
|
resourceMetadataUrl
|
|
1823
2234
|
});
|
|
1824
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);
|
|
1825
2238
|
const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
|
|
1826
|
-
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] };
|
|
1827
2241
|
const { subject, ownerDids } = identity;
|
|
1828
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2242
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1829
2243
|
const validator = createJsonSchemaValidator();
|
|
1830
2244
|
const server = new McpServer2(
|
|
1831
2245
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1832
2246
|
{
|
|
1833
2247
|
jsonSchemaValidator: validator,
|
|
1834
|
-
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."
|
|
1835
2249
|
}
|
|
1836
2250
|
);
|
|
1837
2251
|
registerTinyCloudTools(server, {
|
|
@@ -1840,10 +2254,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1840
2254
|
explicitProfile: true,
|
|
1841
2255
|
allowOwnerProfile: false,
|
|
1842
2256
|
stateRoot,
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
+
} : {}
|
|
1845
2264
|
}, validator);
|
|
1846
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2265
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1847
2266
|
return server;
|
|
1848
2267
|
}, {
|
|
1849
2268
|
legacy: "stateless",
|
|
@@ -1974,7 +2393,8 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1974
2393
|
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
1975
2394
|
}
|
|
1976
2395
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1977
|
-
|
|
2396
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2397
|
+
const card = {
|
|
1978
2398
|
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
1979
2399
|
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
1980
2400
|
title: "TinyCloud MCP",
|
|
@@ -1983,7 +2403,38 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1983
2403
|
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
1984
2404
|
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
1985
2405
|
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
1986
|
-
}
|
|
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);
|
|
1987
2438
|
}
|
|
1988
2439
|
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
1989
2440
|
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
@@ -2081,6 +2532,73 @@ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`
|
|
|
2081
2532
|
);
|
|
2082
2533
|
}
|
|
2083
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
|
+
}
|
|
2084
2602
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
2085
2603
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
2086
2604
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -2192,6 +2710,39 @@ function callbackCors(response, openkeyHost) {
|
|
|
2192
2710
|
headers.set("vary", "Origin");
|
|
2193
2711
|
return new Response(response.body, { status: response.status, headers });
|
|
2194
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
|
+
}
|
|
2195
2746
|
function json(value, status = 200) {
|
|
2196
2747
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
2197
2748
|
}
|