@tinycloud/mcp 0.8.3-beta.9 → 0.8.4-beta.0

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/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.9",
525
+ version: "0.8.4-beta.0",
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.7",
596
+ "@tinycloud/node-sdk": "3.8.1-beta.0",
597
597
  "@tinycloud/node-sdk-wasm": "1.7.8",
598
- "@tinycloud/operations": "0.7.7-beta.8",
598
+ "@tinycloud/operations": "0.7.8-beta.0",
599
599
  jose: "6.2.4"
600
600
  },
601
601
  devDependencies: {
@@ -972,16 +972,85 @@ function claims(token) {
972
972
  }
973
973
 
974
974
  // src/remote-store.ts
975
- var import_node_crypto2 = require("crypto");
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, import_node_crypto2.randomBytes)(24).toString("base64url");
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, import_node_crypto2.timingSafeEqual)(supplied, expectedBytes)) {
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, import_node_crypto2.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
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, import_node_crypto2.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
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, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
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 {
@@ -1795,13 +2199,19 @@ function samePendingRequest(pending, expected) {
1795
2199
 
1796
2200
  // src/http.ts
1797
2201
  var REQUIRED_SCOPE = "tinycloud:mcp";
2202
+ var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
2203
+ var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
1798
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
+ }
1799
2208
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1800
2209
  const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1801
2210
  const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
1802
2211
  const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
1803
2212
  const plugin = createHostedSurface({
1804
2213
  ...config,
2214
+ nativeServiceCredential: void 0,
1805
2215
  stateDir: (0, import_node_path4.join)(config.stateDir, "chatgpt"),
1806
2216
  approvalPath: new URL("./connect", pluginUrl).pathname
1807
2217
  }, pluginUrl, "public", statusFor(pluginUrl));
@@ -1837,16 +2247,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1837
2247
  resourceMetadataUrl
1838
2248
  });
1839
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);
1840
2252
  const handler = (0, import_server5.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
1841
- const identity = oauthIdentity(authInfo2);
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] };
1842
2255
  const { subject, ownerDids } = identity;
1843
- const stateRoot = await store.initializeTenant(subject);
2256
+ const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
1844
2257
  const validator = createJsonSchemaValidator();
1845
2258
  const server = new import_server5.McpServer(
1846
2259
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1847
2260
  {
1848
2261
  jsonSchemaValidator: validator,
1849
- 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."
1850
2263
  }
1851
2264
  );
1852
2265
  registerTinyCloudTools(server, {
@@ -1855,10 +2268,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1855
2268
  explicitProfile: true,
1856
2269
  allowOwnerProfile: false,
1857
2270
  stateRoot,
1858
- beforeOperation: () => store.validateTenant(subject, ownerDids),
1859
- transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
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
+ } : {}
1860
2278
  }, validator);
1861
- registerConnectTool(server, validator, store, identity, surface);
2279
+ if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
1862
2280
  return server;
1863
2281
  }, {
1864
2282
  legacy: "stateless",
@@ -1874,14 +2292,205 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1874
2292
  const allowedHosts = [resourceUrl.hostname];
1875
2293
  return {
1876
2294
  async fetch(request) {
1877
- const metadata = (0, import_server5.oauthMetadataResponse)(request, metadataOptions);
1878
- if (metadata !== void 0) return metadata;
1879
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
+ }
1880
2491
  if (url.pathname === "/healthz") {
1881
2492
  return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
1882
2493
  }
1883
- const hostFailure = (0, import_server5.hostHeaderValidationResponse)(request, allowedHosts);
1884
- if (hostFailure !== void 0) return hostFailure;
1885
2494
  if (url.pathname === connectPath) {
1886
2495
  if (request.method !== "GET") return methodNotAllowed("GET");
1887
2496
  try {
@@ -1937,6 +2546,73 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1937
2546
  );
1938
2547
  }
1939
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
+ }
1940
2616
  if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
1941
2617
  const originFailure = validateOrigin(request, config.allowedOrigins);
1942
2618
  if (originFailure !== void 0) return originFailure;
@@ -2048,9 +2724,77 @@ function callbackCors(response, openkeyHost) {
2048
2724
  headers.set("vary", "Origin");
2049
2725
  return new Response(response.body, { status: response.status, headers });
2050
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
+ }
2051
2760
  function json(value, status = 200) {
2052
2761
  return Response.json(value, { status, headers: { "cache-control": "no-store" } });
2053
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
+ }
2054
2798
  function methodNotAllowed(allow) {
2055
2799
  return new Response("Method not allowed", { status: 405, headers: { allow } });
2056
2800
  }