@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.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.17",
|
|
526
526
|
description: "TinyCloud delegated operations MCP server",
|
|
527
527
|
keywords: [
|
|
528
528
|
"tinycloud",
|
|
@@ -972,16 +972,85 @@ function claims(token) {
|
|
|
972
972
|
}
|
|
973
973
|
|
|
974
974
|
// src/remote-store.ts
|
|
975
|
-
var
|
|
975
|
+
var import_node_crypto3 = require("crypto");
|
|
976
976
|
var import_promises = require("fs/promises");
|
|
977
977
|
var import_node_path3 = require("path");
|
|
978
978
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
979
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");
|
|
980
982
|
var import_operations2 = require("@tinycloud/operations");
|
|
981
983
|
var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
|
|
982
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
|
|
983
1045
|
var PROFILE = "agent";
|
|
984
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
|
+
]);
|
|
985
1054
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
986
1055
|
var BOOTSTRAP_PERMISSIONS = [
|
|
987
1056
|
{
|
|
@@ -1023,6 +1092,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
1023
1092
|
this.detail = detail;
|
|
1024
1093
|
}
|
|
1025
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
|
+
};
|
|
1026
1104
|
var RemoteTenantStore = class {
|
|
1027
1105
|
#config;
|
|
1028
1106
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -1035,9 +1113,186 @@ var RemoteTenantStore = class {
|
|
|
1035
1113
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
1036
1114
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
1037
1115
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
1116
|
+
#nativeIdentity;
|
|
1117
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
1038
1118
|
constructor(config) {
|
|
1039
1119
|
this.#config = config;
|
|
1040
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
|
+
}
|
|
1041
1296
|
tenantStateRoot(subject) {
|
|
1042
1297
|
return (0, import_node_path3.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
1043
1298
|
}
|
|
@@ -1564,7 +1819,7 @@ var RemoteTenantStore = class {
|
|
|
1564
1819
|
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
1565
1820
|
);
|
|
1566
1821
|
if (reusable !== void 0) return reusable;
|
|
1567
|
-
const nonce = (0,
|
|
1822
|
+
const nonce = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
|
|
1568
1823
|
const created = Date.now();
|
|
1569
1824
|
const pending = {
|
|
1570
1825
|
version: 1,
|
|
@@ -1589,7 +1844,7 @@ var RemoteTenantStore = class {
|
|
|
1589
1844
|
const expected = this.#signState(tenantId, nonce);
|
|
1590
1845
|
const supplied = Buffer.from(signature, "base64url");
|
|
1591
1846
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
1592
|
-
if (supplied.length !== expectedBytes.length || !(0,
|
|
1847
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto3.timingSafeEqual)(supplied, expectedBytes)) {
|
|
1593
1848
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1594
1849
|
}
|
|
1595
1850
|
const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -1640,11 +1895,128 @@ var RemoteTenantStore = class {
|
|
|
1640
1895
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
1641
1896
|
return url.toString();
|
|
1642
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
|
+
}
|
|
1643
2015
|
#tenantId(subject) {
|
|
1644
|
-
return (0,
|
|
2016
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
1645
2017
|
}
|
|
1646
2018
|
#signState(tenantId, nonce) {
|
|
1647
|
-
return (0,
|
|
2019
|
+
return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
1648
2020
|
}
|
|
1649
2021
|
#profilePath(stateRoot) {
|
|
1650
2022
|
return (0, import_node_path3.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
@@ -1733,6 +2105,21 @@ function samePrincipal(left, right) {
|
|
|
1733
2105
|
function normalizeDid(value) {
|
|
1734
2106
|
return value.split("#", 1)[0];
|
|
1735
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
|
+
}
|
|
1736
2123
|
function isPermissionEntry(value) {
|
|
1737
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");
|
|
1738
2125
|
}
|
|
@@ -1746,7 +2133,7 @@ async function secureDirectory(path) {
|
|
|
1746
2133
|
async function secureWrite(path, value) {
|
|
1747
2134
|
const directory = (0, import_node_path3.dirname)(path);
|
|
1748
2135
|
await secureDirectory(directory);
|
|
1749
|
-
const temporary = `${path}.${process.pid}.${(0,
|
|
2136
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`;
|
|
1750
2137
|
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
1751
2138
|
`, { mode: 384 });
|
|
1752
2139
|
await (0, import_promises.rename)(temporary, path);
|
|
@@ -1763,6 +2150,23 @@ async function readJson(path) {
|
|
|
1763
2150
|
throw error;
|
|
1764
2151
|
}
|
|
1765
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
|
+
}
|
|
1766
2170
|
async function reusablePending(directory, expected) {
|
|
1767
2171
|
let names;
|
|
1768
2172
|
try {
|
|
@@ -1798,12 +2202,16 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1798
2202
|
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
1799
2203
|
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1800
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
|
+
}
|
|
1801
2208
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1802
2209
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1803
2210
|
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1804
2211
|
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1805
2212
|
const plugin = createHostedSurface({
|
|
1806
2213
|
...config,
|
|
2214
|
+
nativeServiceCredential: void 0,
|
|
1807
2215
|
stateDir: (0, import_node_path4.join)(config.stateDir, "chatgpt"),
|
|
1808
2216
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1809
2217
|
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
@@ -1839,16 +2247,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1839
2247
|
resourceMetadataUrl
|
|
1840
2248
|
});
|
|
1841
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);
|
|
1842
2252
|
const handler = (0, import_server5.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
|
|
1843
|
-
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] };
|
|
1844
2255
|
const { subject, ownerDids } = identity;
|
|
1845
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2256
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1846
2257
|
const validator = createJsonSchemaValidator();
|
|
1847
2258
|
const server = new import_server5.McpServer(
|
|
1848
2259
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1849
2260
|
{
|
|
1850
2261
|
jsonSchemaValidator: validator,
|
|
1851
|
-
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."
|
|
1852
2263
|
}
|
|
1853
2264
|
);
|
|
1854
2265
|
registerTinyCloudTools(server, {
|
|
@@ -1857,10 +2268,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1857
2268
|
explicitProfile: true,
|
|
1858
2269
|
allowOwnerProfile: false,
|
|
1859
2270
|
stateRoot,
|
|
1860
|
-
|
|
1861
|
-
|
|
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
|
+
} : {}
|
|
1862
2278
|
}, validator);
|
|
1863
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2279
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1864
2280
|
return server;
|
|
1865
2281
|
}, {
|
|
1866
2282
|
legacy: "stateless",
|
|
@@ -1991,7 +2407,8 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
1991
2407
|
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
1992
2408
|
}
|
|
1993
2409
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1994
|
-
|
|
2410
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2411
|
+
const card = {
|
|
1995
2412
|
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
1996
2413
|
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
1997
2414
|
title: "TinyCloud MCP",
|
|
@@ -2000,7 +2417,38 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
|
2000
2417
|
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
2001
2418
|
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
2002
2419
|
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
2003
|
-
}
|
|
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);
|
|
2004
2452
|
}
|
|
2005
2453
|
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
2006
2454
|
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
@@ -2098,6 +2546,73 @@ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`
|
|
|
2098
2546
|
);
|
|
2099
2547
|
}
|
|
2100
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
|
+
}
|
|
2101
2616
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
2102
2617
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
2103
2618
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -2209,6 +2724,39 @@ function callbackCors(response, openkeyHost) {
|
|
|
2209
2724
|
headers.set("vary", "Origin");
|
|
2210
2725
|
return new Response(response.body, { status: response.status, headers });
|
|
2211
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
|
+
}
|
|
2212
2760
|
function json(value, status = 200) {
|
|
2213
2761
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
2214
2762
|
}
|