@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.js CHANGED
@@ -407,7 +407,7 @@ function createJsonSchemaValidator() {
407
407
  return new AjvJsonSchemaValidator();
408
408
  }
409
409
  function registerTinyCloudTools(server, selection, validator) {
410
- const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
410
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
411
411
  for (const binding of bindings) {
412
412
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
413
413
  if (operation === void 0) {
@@ -485,7 +485,7 @@ function outcomeCode(result) {
485
485
  // package.json
486
486
  var package_default = {
487
487
  name: "@tinycloud/mcp",
488
- version: "0.8.3-beta.9",
488
+ version: "0.8.4-beta.0",
489
489
  description: "TinyCloud delegated operations MCP server",
490
490
  keywords: [
491
491
  "tinycloud",
@@ -556,9 +556,9 @@ var package_default = {
556
556
  dependencies: {
557
557
  "@modelcontextprotocol/node": "2.0.0-beta.5",
558
558
  "@modelcontextprotocol/server": "2.0.0-beta.5",
559
- "@tinycloud/node-sdk": "3.8.0-beta.7",
559
+ "@tinycloud/node-sdk": "3.8.1-beta.0",
560
560
  "@tinycloud/node-sdk-wasm": "1.7.8",
561
- "@tinycloud/operations": "0.7.7-beta.8",
561
+ "@tinycloud/operations": "0.7.8-beta.0",
562
562
  jose: "6.2.4"
563
563
  },
564
564
  devDependencies: {
@@ -946,12 +946,15 @@ function claims(token) {
946
946
  }
947
947
 
948
948
  // src/remote-store.ts
949
- import { createHmac, randomBytes, timingSafeEqual } from "crypto";
949
+ import { createHmac, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
950
950
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
951
951
  import { dirname, join as join2 } from "path";
952
952
  import {
953
+ activateValidatedRuntimeDelegation,
954
+ deserializeDelegation,
953
955
  InvalidRestoredSessionError,
954
956
  RuntimeDelegationRejectedError,
957
+ serializeDelegation,
955
958
  TinyCloudNode,
956
959
  UnsupportedSessionRestoreError,
957
960
  isCapabilitySubset,
@@ -960,11 +963,80 @@ import {
960
963
  signedCompactUcanExpiry
961
964
  } from "@tinycloud/node-sdk";
962
965
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
966
+ import { createOrReusePermissionRequest } from "@tinycloud/operations/artifacts";
967
+ import { withTinyCloudStateRoot } from "@tinycloud/operations/state";
963
968
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
964
969
  import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
965
970
  import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
971
+
972
+ // src/native-auth.ts
973
+ import { timingSafeEqual } from "crypto";
974
+ var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
975
+ var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
976
+ var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
977
+ var NATIVE_GRANT_REQUIRED = "native_grant_required";
978
+ var NATIVE_GRANT_EXPIRED = "native_grant_expired";
979
+ var NATIVE_GRANT_REJECTED = "native_grant_rejected";
980
+ var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
981
+ var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
982
+ var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
983
+ var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
984
+ function serviceCredentialMatches(supplied, configured) {
985
+ if (supplied === null || configured === void 0) return false;
986
+ const suppliedBytes = Buffer.from(supplied, "utf8");
987
+ const configuredBytes = Buffer.from(configured, "utf8");
988
+ return suppliedBytes.byteLength === configuredBytes.byteLength && timingSafeEqual(suppliedBytes, configuredBytes);
989
+ }
990
+ var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
991
+ var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
992
+ var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
993
+ function canonicalOwnerDid(value) {
994
+ const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
995
+ if (!match) return void 0;
996
+ const chainId = Number(match[1]);
997
+ if (!Number.isSafeInteger(chainId)) return void 0;
998
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
999
+ }
1000
+ function nativeOwnerForConnectorGrant(permissions) {
1001
+ if (permissions.length !== 2) return void 0;
1002
+ const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
1003
+ const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
1004
+ if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
1005
+ const sql = sqlEntries[0];
1006
+ const kv = kvEntries[0];
1007
+ if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
1008
+ if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
1009
+ if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
1010
+ const sqlSpace = ownerFromApplicationsSpace(sql.space);
1011
+ const kvSpace = ownerFromApplicationsSpace(kv.space);
1012
+ return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
1013
+ }
1014
+ function ownerFromApplicationsSpace(space) {
1015
+ const match = OWNER_APPLICATIONS_SPACE.exec(space);
1016
+ if (!match) return void 0;
1017
+ const chainId = Number(match[1]);
1018
+ if (!Number.isSafeInteger(chainId)) return void 0;
1019
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
1020
+ }
1021
+ function actionsEqual(actual, expected) {
1022
+ const actualActions = [...actual].sort();
1023
+ const expectedActions = [...expected].sort();
1024
+ return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
1025
+ }
1026
+ function hasCaveats(permission) {
1027
+ return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
1028
+ }
1029
+
1030
+ // src/remote-store.ts
966
1031
  var PROFILE = "agent";
967
1032
  var MAX_CALLBACK_BYTES = 256 * 1024;
1033
+ var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
1034
+ "DELEGATION_ARTIFACT_INVALID",
1035
+ "DELEGATION_EXPIRED",
1036
+ "DELEGATION_AUDIENCE_MISMATCH",
1037
+ "DELEGATION_HOST_MISMATCH",
1038
+ "DELEGATION_REJECTED"
1039
+ ]);
968
1040
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
969
1041
  var BOOTSTRAP_PERMISSIONS = [
970
1042
  {
@@ -1006,6 +1078,15 @@ var ApprovalRequestError = class extends Error {
1006
1078
  this.detail = detail;
1007
1079
  }
1008
1080
  };
1081
+ var NativeGrantValidationError = class extends Error {
1082
+ code;
1083
+ constructor(status) {
1084
+ const code = status.startsWith("native_") ? status : status === "required" ? "native_grant_required" : status === "expired" ? "native_grant_expired" : status === "unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected";
1085
+ super(code);
1086
+ this.name = "NativeGrantValidationError";
1087
+ this.code = code;
1088
+ }
1089
+ };
1009
1090
  var RemoteTenantStore = class {
1010
1091
  #config;
1011
1092
  #initializing = /* @__PURE__ */ new Map();
@@ -1018,9 +1099,186 @@ var RemoteTenantStore = class {
1018
1099
  #scopedClearing = /* @__PURE__ */ new Map();
1019
1100
  #clearedScoped = /* @__PURE__ */ new Map();
1020
1101
  #approvalProcessing = /* @__PURE__ */ new Map();
1102
+ #nativeIdentity;
1103
+ #nativeGrantQueues = /* @__PURE__ */ new Map();
1021
1104
  constructor(config) {
1022
1105
  this.#config = config;
1023
1106
  }
1107
+ async nativeServiceIdentity() {
1108
+ const identity = await this.#loadNativeServiceKey();
1109
+ return { serviceDid: normalizeDid(identity.did) };
1110
+ }
1111
+ nativeTenantStateRoot(ownerDid) {
1112
+ const canonical = canonicalOwnerDid(ownerDid);
1113
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
1114
+ const tenant = createHmac("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
1115
+ return join2(this.#config.stateDir, "native-tenants", tenant);
1116
+ }
1117
+ async acceptNativeGrant(serializedDelegation) {
1118
+ if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
1119
+ throw new NativeGrantValidationError("rejected");
1120
+ }
1121
+ const identity = await this.#loadNativeServiceKey();
1122
+ const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
1123
+ const { ownerDid, spaceId, permissions, expiresAt } = activated;
1124
+ return this.#withNativeGrantLock(ownerDid, async () => {
1125
+ const stateRoot = this.nativeTenantStateRoot(ownerDid);
1126
+ let previous = null;
1127
+ try {
1128
+ const stored = await readJson(this.#nativeGrantPath(stateRoot));
1129
+ if (isRecord(stored) && stored.version === 1 && stored.ownerDid === ownerDid && typeof stored.serviceDid === "string" && samePrincipal(stored.serviceDid, identity.did) && typeof stored.serializedDelegation === "string" && typeof stored.delegationCid === "string" && typeof stored.spaceId === "string" && Array.isArray(stored.permissions) && stored.permissions.every(isPermissionEntry) && nativeOwnerForConnectorGrant(stored.permissions) === ownerDid && typeof stored.expiresAt === "string" && Number.isFinite(Date.parse(stored.expiresAt))) {
1130
+ previous = stored;
1131
+ }
1132
+ } catch (error) {
1133
+ if (!(error instanceof SyntaxError)) throw error;
1134
+ }
1135
+ if (previous !== null) {
1136
+ const previousExpiry = Date.parse(previous.expiresAt);
1137
+ const nextExpiry = expiresAt.getTime();
1138
+ if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
1139
+ throw new NativeGrantValidationError("rejected");
1140
+ }
1141
+ if (previous.delegationCid === activated.cid) {
1142
+ return {
1143
+ ownerDid,
1144
+ serviceDid: normalizeDid(identity.did),
1145
+ expiresAt: previous.expiresAt,
1146
+ stateRoot
1147
+ };
1148
+ }
1149
+ }
1150
+ const session = {
1151
+ delegationHeader: activated.delegation.delegationHeader,
1152
+ delegationCid: activated.cid,
1153
+ spaceId,
1154
+ jwk: identity.jwk,
1155
+ verificationMethod: identity.did,
1156
+ tinycloudHosts: [this.#config.nodeHost]
1157
+ };
1158
+ const grant = {
1159
+ version: 1,
1160
+ ownerDid,
1161
+ serviceDid: normalizeDid(identity.did),
1162
+ serializedDelegation: serializeDelegation(activated.delegation),
1163
+ delegationCid: activated.cid,
1164
+ spaceId,
1165
+ permissions,
1166
+ expiresAt: expiresAt.toISOString()
1167
+ };
1168
+ const sessionPath = this.#sessionPath(stateRoot);
1169
+ const previousSession = await readOptionalText(sessionPath);
1170
+ try {
1171
+ await secureWrite(this.#profilePath(stateRoot), {
1172
+ name: PROFILE,
1173
+ host: this.#config.nodeHost,
1174
+ chainId: activated.delegation.chainId,
1175
+ spaceName: "applications",
1176
+ did: identity.did,
1177
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1178
+ profile: PROFILE,
1179
+ operatorType: "agent",
1180
+ posture: "delegate-session",
1181
+ ownerDid,
1182
+ sessionDid: identity.did,
1183
+ spaceId,
1184
+ tinycloudHost: this.#config.nodeHost
1185
+ });
1186
+ await secureWrite(this.#keyPath(stateRoot), identity.jwk);
1187
+ await secureWrite(sessionPath, session);
1188
+ const request = await withTinyCloudStateRoot(stateRoot, () => createOrReusePermissionRequest({
1189
+ profile: PROFILE,
1190
+ posture: "delegate-session",
1191
+ operatorType: "agent",
1192
+ host: this.#config.nodeHost,
1193
+ sessionDid: identity.did,
1194
+ ownerDid,
1195
+ spaceId,
1196
+ missing: permissions,
1197
+ granted: [],
1198
+ replace: true
1199
+ }));
1200
+ if (request.status !== "created") {
1201
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
1202
+ }
1203
+ const imported = await invokeOperation2(
1204
+ "tinycloud.auth.import",
1205
+ 1,
1206
+ { profile: PROFILE, stateRoot },
1207
+ {
1208
+ kind: "tinycloud.auth.delegation",
1209
+ version: 1,
1210
+ requestId: request.request.requestId,
1211
+ delegationCid: activated.cid,
1212
+ delegation: activated.delegation
1213
+ }
1214
+ );
1215
+ if (imported.status === "error") {
1216
+ throw new NativeGrantValidationError(
1217
+ NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
1218
+ );
1219
+ }
1220
+ if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
1221
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
1222
+ }
1223
+ await secureWrite(this.#nativeGrantPath(stateRoot), grant);
1224
+ } catch (error) {
1225
+ try {
1226
+ await restoreOptionalText(sessionPath, previousSession);
1227
+ } catch {
1228
+ await rm(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
1229
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
1230
+ }
1231
+ throw error;
1232
+ }
1233
+ return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
1234
+ });
1235
+ }
1236
+ async nativeGrantForOwner(ownerDid) {
1237
+ const canonical = canonicalOwnerDid(ownerDid);
1238
+ if (canonical === void 0) return { status: "rejected" };
1239
+ const stateRoot = this.nativeTenantStateRoot(canonical);
1240
+ let stored;
1241
+ try {
1242
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
1243
+ } catch {
1244
+ return { status: "rejected" };
1245
+ }
1246
+ if (stored === null) return { status: "required" };
1247
+ if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.serviceDid !== "string" || typeof stored.serializedDelegation !== "string" || typeof stored.delegationCid !== "string" || typeof stored.spaceId !== "string" || typeof stored.expiresAt !== "string" || !Array.isArray(stored.permissions) || !stored.permissions.every(isPermissionEntry) || nativeOwnerForConnectorGrant(stored.permissions) !== canonical) {
1248
+ return { status: "rejected" };
1249
+ }
1250
+ const expires = Date.parse(stored.expiresAt);
1251
+ if (!Number.isFinite(expires)) return { status: "rejected" };
1252
+ if (expires <= Date.now()) return { status: "expired" };
1253
+ try {
1254
+ const identity = await this.#loadNativeServiceKey();
1255
+ if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
1256
+ const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
1257
+ if (activated.ownerDid !== canonical || activated.cid !== stored.delegationCid || activated.spaceId !== stored.spaceId || JSON.stringify(activated.permissions) !== JSON.stringify(stored.permissions) || activated.expiresAt.toISOString() !== stored.expiresAt) return { status: "rejected" };
1258
+ return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
1259
+ } catch (error) {
1260
+ if (error instanceof NativeGrantValidationError) {
1261
+ return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
1262
+ }
1263
+ return { status: "unavailable" };
1264
+ }
1265
+ }
1266
+ async assertNativeGrantActive(ownerDid) {
1267
+ const canonical = canonicalOwnerDid(ownerDid);
1268
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
1269
+ const stateRoot = this.nativeTenantStateRoot(canonical);
1270
+ let stored;
1271
+ try {
1272
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
1273
+ } catch {
1274
+ throw new NativeGrantValidationError("rejected");
1275
+ }
1276
+ if (stored === null) throw new NativeGrantValidationError("required");
1277
+ if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
1278
+ throw new NativeGrantValidationError("rejected");
1279
+ }
1280
+ if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
1281
+ }
1024
1282
  tenantStateRoot(subject) {
1025
1283
  return join2(this.#config.stateDir, "tenants", this.#tenantId(subject));
1026
1284
  }
@@ -1572,7 +1830,7 @@ var RemoteTenantStore = class {
1572
1830
  const expected = this.#signState(tenantId, nonce);
1573
1831
  const supplied = Buffer.from(signature, "base64url");
1574
1832
  const expectedBytes = Buffer.from(expected, "base64url");
1575
- if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
1833
+ if (supplied.length !== expectedBytes.length || !timingSafeEqual2(supplied, expectedBytes)) {
1576
1834
  throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1577
1835
  }
1578
1836
  const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
@@ -1623,6 +1881,123 @@ var RemoteTenantStore = class {
1623
1881
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
1624
1882
  return url.toString();
1625
1883
  }
1884
+ async #withNativeGrantLock(ownerDid, action) {
1885
+ const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
1886
+ let releaseNext;
1887
+ const next = new Promise((resolve) => {
1888
+ releaseNext = resolve;
1889
+ });
1890
+ this.#nativeGrantQueues.set(ownerDid, next);
1891
+ await previous;
1892
+ try {
1893
+ return await action();
1894
+ } finally {
1895
+ releaseNext();
1896
+ if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
1897
+ }
1898
+ }
1899
+ async #loadNativeServiceKey() {
1900
+ if (this.#nativeIdentity === void 0) {
1901
+ const loading = this.#readOrCreateNativeServiceKey();
1902
+ this.#nativeIdentity = loading;
1903
+ void loading.catch(() => {
1904
+ if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
1905
+ });
1906
+ }
1907
+ return this.#nativeIdentity;
1908
+ }
1909
+ async #readOrCreateNativeServiceKey() {
1910
+ const path = join2(this.#config.stateDir, ".tinycloud", "native-service-key.json");
1911
+ await secureDirectory(dirname(path));
1912
+ let stored = await readJson(path);
1913
+ if (stored === null) {
1914
+ const generated = generateSessionKey();
1915
+ try {
1916
+ await writeFile(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
1917
+ `, {
1918
+ mode: 384,
1919
+ flag: "wx"
1920
+ });
1921
+ } catch (error) {
1922
+ if (!isRecord(error) || error.code !== "EEXIST") throw error;
1923
+ }
1924
+ await chmod(path, 384);
1925
+ stored = await readJson(path);
1926
+ }
1927
+ if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
1928
+ throw new Error("The native MCP service identity is unavailable.");
1929
+ }
1930
+ return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
1931
+ }
1932
+ async #activateNativeDelegation(serialized, identity) {
1933
+ let delegation;
1934
+ try {
1935
+ const parsed = JSON.parse(serialized);
1936
+ if (!isRecord(parsed)) throw new Error("invalid delegation");
1937
+ delegation = deserializeDelegation(serialized);
1938
+ } catch {
1939
+ throw new NativeGrantValidationError("native_grant_rejected");
1940
+ }
1941
+ if (!isRecord(delegation.delegationHeader) || typeof delegation.delegationHeader.Authorization !== "string" || delegation.delegationHeader.Authorization.length === 0 || typeof delegation.cid !== "string" || delegation.cid.length === 0 || typeof delegation.delegateDID !== "string" || !samePrincipal(delegation.delegateDID, normalizeDid(identity.did)) || typeof delegation.spaceId !== "string" || typeof delegation.ownerAddress !== "string" || !Number.isSafeInteger(delegation.chainId) || typeof delegation.host !== "string" || !sameNodeHost(delegation.host, this.#config.nodeHost) || !(delegation.expiry instanceof Date) || !Number.isFinite(delegation.expiry.getTime())) {
1942
+ throw new NativeGrantValidationError("native_grant_rejected");
1943
+ }
1944
+ const now = Date.now();
1945
+ const claimedExpiry = delegation.expiry.getTime();
1946
+ if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
1947
+ if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1948
+ throw new NativeGrantValidationError("native_grant_rejected");
1949
+ }
1950
+ let ownerDid;
1951
+ let spaceId;
1952
+ const node = new TinyCloudNode({ host: this.#config.nodeHost });
1953
+ try {
1954
+ await node.restoreSession({
1955
+ delegationHeader: delegation.delegationHeader,
1956
+ delegationCid: delegation.cid,
1957
+ spaceId: delegation.spaceId,
1958
+ jwk: identity.jwk,
1959
+ verificationMethod: identity.did,
1960
+ tinycloudHosts: [this.#config.nodeHost]
1961
+ });
1962
+ const activated = await activateValidatedRuntimeDelegation(node, delegation, {
1963
+ host: this.#config.nodeHost,
1964
+ authorize: (permissions) => {
1965
+ const derivedOwner = nativeOwnerForConnectorGrant(permissions);
1966
+ const signedSpace = permissions[0]?.space;
1967
+ if (derivedOwner === void 0 || signedSpace === void 0) return false;
1968
+ const metadataOwner = canonicalOwnerDid(
1969
+ `did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
1970
+ );
1971
+ if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
1972
+ ownerDid = derivedOwner;
1973
+ spaceId = signedSpace;
1974
+ return true;
1975
+ }
1976
+ });
1977
+ if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1978
+ throw new NativeGrantValidationError("native_grant_rejected");
1979
+ }
1980
+ return {
1981
+ ownerDid,
1982
+ spaceId,
1983
+ permissions: [...activated.effectivePermissions],
1984
+ cid: activated.cid,
1985
+ expiresAt: activated.expiry,
1986
+ delegation: activated.delegation
1987
+ };
1988
+ } catch (error) {
1989
+ if (error instanceof NativeGrantValidationError) throw error;
1990
+ if (error instanceof RuntimeDelegationRejectedError) {
1991
+ throw new NativeGrantValidationError(
1992
+ error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
1993
+ );
1994
+ }
1995
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
1996
+ }
1997
+ }
1998
+ #nativeGrantPath(stateRoot) {
1999
+ return join2(stateRoot, ".tinycloud", "native-grant.json");
2000
+ }
1626
2001
  #tenantId(subject) {
1627
2002
  return createHmac("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
1628
2003
  }
@@ -1716,6 +2091,21 @@ function samePrincipal(left, right) {
1716
2091
  function normalizeDid(value) {
1717
2092
  return value.split("#", 1)[0];
1718
2093
  }
2094
+ function sameNodeHost(left, right) {
2095
+ try {
2096
+ const normalize = (value) => {
2097
+ const url = new URL(value);
2098
+ if (url.username !== "" || url.password !== "") return "";
2099
+ url.hash = "";
2100
+ url.search = "";
2101
+ url.pathname = url.pathname.replace(/\/+$/, "");
2102
+ return url.toString().replace(/\/+$/, "");
2103
+ };
2104
+ return normalize(left) !== "" && normalize(left) === normalize(right);
2105
+ } catch {
2106
+ return false;
2107
+ }
2108
+ }
1719
2109
  function isPermissionEntry(value) {
1720
2110
  return isRecord(value) && typeof value.service === "string" && typeof value.space === "string" && typeof value.path === "string" && Array.isArray(value.actions) && value.actions.every((action) => typeof action === "string");
1721
2111
  }
@@ -1746,6 +2136,23 @@ async function readJson(path) {
1746
2136
  throw error;
1747
2137
  }
1748
2138
  }
2139
+ async function readOptionalText(path) {
2140
+ try {
2141
+ return await readFile(path, "utf8");
2142
+ } catch (error) {
2143
+ if (isRecord(error) && error.code === "ENOENT") return null;
2144
+ throw error;
2145
+ }
2146
+ }
2147
+ async function restoreOptionalText(path, contents) {
2148
+ if (contents === null) {
2149
+ await rm(path, { force: true });
2150
+ return;
2151
+ }
2152
+ await secureDirectory(dirname(path));
2153
+ await writeFile(path, contents, { mode: 384 });
2154
+ await chmod(path, 384);
2155
+ }
1749
2156
  async function reusablePending(directory, expected) {
1750
2157
  let names;
1751
2158
  try {
@@ -1778,13 +2185,19 @@ function samePendingRequest(pending, expected) {
1778
2185
 
1779
2186
  // src/http.ts
1780
2187
  var REQUIRED_SCOPE = "tinycloud:mcp";
2188
+ var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
2189
+ var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
1781
2190
  function createHostedMcpApp(config) {
2191
+ if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
2192
+ throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
2193
+ }
1782
2194
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1783
2195
  const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1784
2196
  const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
1785
2197
  const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
1786
2198
  const plugin = createHostedSurface({
1787
2199
  ...config,
2200
+ nativeServiceCredential: void 0,
1788
2201
  stateDir: join3(config.stateDir, "chatgpt"),
1789
2202
  approvalPath: new URL("./connect", pluginUrl).pathname
1790
2203
  }, pluginUrl, "public", statusFor(pluginUrl));
@@ -1820,16 +2233,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1820
2233
  resourceMetadataUrl
1821
2234
  });
1822
2235
  const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
2236
+ const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
2237
+ if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
1823
2238
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1824
- const identity = oauthIdentity(authInfo2);
2239
+ const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
2240
+ const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
1825
2241
  const { subject, ownerDids } = identity;
1826
- const stateRoot = await store.initializeTenant(subject);
2242
+ const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
1827
2243
  const validator = createJsonSchemaValidator();
1828
2244
  const server = new McpServer2(
1829
2245
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1830
2246
  {
1831
2247
  jsonSchemaValidator: validator,
1832
- instructions: surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first."
2248
+ instructions: nativeOwner === void 0 ? surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first." : "Use only the user's requested connector-transcript data. The signed TinyCloud grant authorizes read-only SQL and transcript KV reads."
1833
2249
  }
1834
2250
  );
1835
2251
  registerTinyCloudTools(server, {
@@ -1838,10 +2254,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1838
2254
  explicitProfile: true,
1839
2255
  allowOwnerProfile: false,
1840
2256
  stateRoot,
1841
- beforeOperation: () => store.validateTenant(subject, ownerDids),
1842
- transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2257
+ ...nativeOwner === void 0 ? {} : {
2258
+ allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
2259
+ },
2260
+ beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
2261
+ ...nativeOwner === void 0 ? {
2262
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2263
+ } : {}
1843
2264
  }, validator);
1844
- registerConnectTool(server, validator, store, identity, surface);
2265
+ if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
1845
2266
  return server;
1846
2267
  }, {
1847
2268
  legacy: "stateless",
@@ -1857,14 +2278,205 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1857
2278
  const allowedHosts = [resourceUrl.hostname];
1858
2279
  return {
1859
2280
  async fetch(request) {
1860
- const metadata = oauthMetadataResponse(request, metadataOptions);
1861
- if (metadata !== void 0) return metadata;
1862
2281
  const url = new URL(request.url);
2282
+ if (url.pathname !== "/healthz") {
2283
+ const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
2284
+ if (hostFailure !== void 0) return hostFailure;
2285
+ }
2286
+ if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
2287
+ const metadataUrl = new URL(getOAuthProtectedResourceMetadataUrl(resourceUrl));
2288
+ const metadataRequest = new Request(metadataUrl.toString(), {
2289
+ method: request.method,
2290
+ headers: request.headers
2291
+ });
2292
+ const response = oauthMetadataResponse(metadataRequest, metadataOptions);
2293
+ if (response !== void 0) return withBearerMethodsSupported(response);
2294
+ }
2295
+ const metadata = oauthMetadataResponse(request, metadataOptions);
2296
+ if (metadata !== void 0) {
2297
+ return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
2298
+ }
2299
+ if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
2300
+ if (request.method === "OPTIONS") {
2301
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2302
+ }
2303
+ if (request.method !== "GET") return methodNotAllowed("GET");
2304
+ return publicMetadataCors(Response.json({
2305
+ specVersion: "1.0",
2306
+ host: {
2307
+ displayName: "TinyCloud, Inc.",
2308
+ identifier: "https://mcp.tinycloud.xyz",
2309
+ documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
2310
+ },
2311
+ entries: [{
2312
+ identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
2313
+ displayName: "TinyCloud MCP",
2314
+ type: "application/json",
2315
+ description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
2316
+ url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
2317
+ representativeQueries: [
2318
+ "How do I connect to TinyCloud's hosted MCP server?",
2319
+ "Which endpoint provides TinyCloud's delegated MCP operations?",
2320
+ "How can I connect to TinyCloud MCP using local stdio?"
2321
+ ]
2322
+ }]
2323
+ }, {
2324
+ headers: {
2325
+ "cache-control": "public, max-age=300",
2326
+ "content-type": "application/json; charset=utf-8"
2327
+ }
2328
+ }), request);
2329
+ }
2330
+ if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
2331
+ if (request.method === "OPTIONS") {
2332
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2333
+ }
2334
+ if (request.method !== "GET") return methodNotAllowed("GET");
2335
+ return publicMetadataCors(Response.json({
2336
+ protocol_version: "0.1",
2337
+ organization: {
2338
+ name: "TinyCloud, Inc.",
2339
+ domain: resourceUrl.hostname
2340
+ },
2341
+ web: {}
2342
+ }, {
2343
+ headers: {
2344
+ "cache-control": "public, max-age=300",
2345
+ "content-type": "application/json; charset=utf-8"
2346
+ }
2347
+ }), request);
2348
+ }
2349
+ if (url.pathname === "/auth.md" && surface === "hosted") {
2350
+ if (request.method === "OPTIONS") {
2351
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2352
+ }
2353
+ if (request.method !== "GET") return methodNotAllowed("GET");
2354
+ const issuerUrl = new URL(issuer);
2355
+ const authorizationServerMetadataUrl = new URL(
2356
+ `/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
2357
+ issuerUrl.origin
2358
+ );
2359
+ const registrationEndpoint = config.oauthMetadata.registration_endpoint;
2360
+ const registrationNote = typeof registrationEndpoint === "string" ? `OpenKey's OAuth \`registration_endpoint\` is ${registrationEndpoint}; it registers OAuth clients, not TinyCloud users or agents.` : "The authorization server does not advertise an OAuth client registration endpoint.";
2361
+ const authDoc = [
2362
+ "# TinyCloud MCP auth.md",
2363
+ "",
2364
+ `Protected resource: ${resourceUrl}.`,
2365
+ "",
2366
+ "Send an OAuth access token in the `Authorization: Bearer` header.",
2367
+ "",
2368
+ `Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
2369
+ `Authorization server issuer: ${issuer}.`,
2370
+ `Authorization server metadata: ${authorizationServerMetadataUrl}.`,
2371
+ `Scope: \`${REQUIRED_SCOPE}\`.`,
2372
+ "",
2373
+ "Hosted access uses OpenKey OAuth.",
2374
+ registrationNote,
2375
+ "TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
2376
+ "No `agent_auth` registration block is published because no agent-registration endpoint is available.",
2377
+ "",
2378
+ `Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
2379
+ "",
2380
+ "Connection steps: https://docs.tinycloud.xyz/cli/mcp"
2381
+ ].join("\n") + "\n";
2382
+ return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
2383
+ }
2384
+ if (url.pathname === "/robots.txt" && surface === "hosted") {
2385
+ return request.method === "GET" ? publicText(`User-agent: *
2386
+ Allow: /
2387
+ Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
2388
+ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
2389
+ `, "text/plain; charset=utf-8") : methodNotAllowed("GET");
2390
+ }
2391
+ if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
2392
+ if (request.method === "OPTIONS") {
2393
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2394
+ }
2395
+ if (request.method !== "GET") return methodNotAllowed("GET");
2396
+ const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
2397
+ const card = {
2398
+ $schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
2399
+ name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
2400
+ title: "TinyCloud MCP",
2401
+ version: MCP_VERSION,
2402
+ description: "MCP server for delegated TinyCloud operations.",
2403
+ websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
2404
+ repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
2405
+ remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
2406
+ };
2407
+ if (identity !== void 0) {
2408
+ card["x-tinycloud-native-auth"] = {
2409
+ serviceDid: identity.serviceDid,
2410
+ serviceDidField: "x-tinycloud-native-auth.serviceDid",
2411
+ grantEndpoint: {
2412
+ method: "POST",
2413
+ path: "/native/grant",
2414
+ body: { delegation: "serialized PortableDelegation JSON string" }
2415
+ },
2416
+ serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
2417
+ serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
2418
+ ownerDidHeader: NATIVE_OWNER_DID_HEADER,
2419
+ maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
2420
+ acceptedCapabilities: [
2421
+ { service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
2422
+ { service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
2423
+ ],
2424
+ tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
2425
+ errors: {
2426
+ required: NATIVE_GRANT_REQUIRED,
2427
+ expired: NATIVE_GRANT_EXPIRED,
2428
+ rejected: NATIVE_GRANT_REJECTED,
2429
+ serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
2430
+ serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
2431
+ validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
2432
+ }
2433
+ };
2434
+ }
2435
+ return publicMetadataCors(Response.json(card, {
2436
+ headers: { "cache-control": "public, max-age=300" }
2437
+ }), request);
2438
+ }
2439
+ if (url.pathname === "/llms.txt" && surface === "hosted") {
2440
+ return request.method === "GET" ? publicText(`# TinyCloud MCP
2441
+
2442
+ Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
2443
+
2444
+ Connection documentation: https://docs.tinycloud.xyz/cli/mcp
2445
+
2446
+ Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
2447
+ `, "text/plain; charset=utf-8") : methodNotAllowed("GET");
2448
+ }
2449
+ if (url.pathname === "/sitemap.xml" && surface === "hosted") {
2450
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${new URL("/", resourceUrl)}</loc></url></urlset>`;
2451
+ return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
2452
+ }
2453
+ if (url.pathname === "/" && surface === "hosted") {
2454
+ if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
2455
+ const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
2456
+ const content = markdown ? `# TinyCloud MCP
2457
+
2458
+ Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
2459
+
2460
+ [Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
2461
+
2462
+ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
2463
+ ` : `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="ai-catalog" href="/.well-known/ai-catalog.json"><title>TinyCloud MCP</title></head><main><h1>TinyCloud MCP</h1><p>Hosted MCP endpoint: <code>https://mcp.tinycloud.xyz/mcp</code> (Streamable HTTP, OpenKey OAuth).</p><p><a href="https://docs.tinycloud.xyz/cli/mcp">Connection documentation</a></p><p>Local alternative: install <code>${LOCAL_CLI_PACKAGE}</code> and <code>${LOCAL_MCP_PACKAGE}</code> and connect the <code>tinycloud-mcp</code> stdio server.</p></main></html>`;
2464
+ const headers = new Headers({
2465
+ "content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
2466
+ "cache-control": "public, max-age=300",
2467
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
2468
+ "x-content-type-options": "nosniff",
2469
+ vary: "Accept"
2470
+ });
2471
+ headers.set(
2472
+ "link",
2473
+ `<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
2474
+ );
2475
+ return new Response(request.method === "HEAD" ? null : content, { headers });
2476
+ }
1863
2477
  if (url.pathname === "/healthz") {
1864
2478
  return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
1865
2479
  }
1866
- const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
1867
- if (hostFailure !== void 0) return hostFailure;
1868
2480
  if (url.pathname === connectPath) {
1869
2481
  if (request.method !== "GET") return methodNotAllowed("GET");
1870
2482
  try {
@@ -1920,6 +2532,73 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1920
2532
  );
1921
2533
  }
1922
2534
  }
2535
+ if (url.pathname === "/native/grant" && surface === "hosted") {
2536
+ if (request.method !== "POST") return methodNotAllowed("POST");
2537
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2538
+ if (config.nativeServiceCredential === void 0) {
2539
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2540
+ }
2541
+ if (!serviceCredentialMatches(
2542
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2543
+ config.nativeServiceCredential
2544
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2545
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
2546
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2547
+ }
2548
+ const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
2549
+ if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
2550
+ let body;
2551
+ try {
2552
+ body = JSON.parse(bodyText);
2553
+ } catch {
2554
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2555
+ }
2556
+ if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
2557
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2558
+ }
2559
+ const grantBody = body;
2560
+ if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
2561
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2562
+ }
2563
+ try {
2564
+ const accepted = await store.acceptNativeGrant(grantBody.delegation);
2565
+ return json({
2566
+ ownerDid: accepted.ownerDid,
2567
+ serviceDid: accepted.serviceDid,
2568
+ expiresAt: accepted.expiresAt
2569
+ }, 201);
2570
+ } catch (error) {
2571
+ return nativeGrantErrorResponse(error);
2572
+ }
2573
+ }
2574
+ if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
2575
+ if (config.nativeServiceCredential === void 0) {
2576
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2577
+ }
2578
+ if (!serviceCredentialMatches(
2579
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2580
+ config.nativeServiceCredential
2581
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2582
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2583
+ const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
2584
+ if (ownerHeader === null || ownerHeader.length === 0) {
2585
+ return json({ error: NATIVE_GRANT_REQUIRED }, 401);
2586
+ }
2587
+ const grant = await store.nativeGrantForOwner(ownerHeader);
2588
+ if (grant.status !== "ok") {
2589
+ const status = grant.status === "unavailable" ? 503 : 401;
2590
+ const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
2591
+ return json({ error: code }, status);
2592
+ }
2593
+ return handler.fetch(request, {
2594
+ authInfo: {
2595
+ token: "native-service",
2596
+ clientId: "tinycloud-native",
2597
+ scopes: [],
2598
+ extra: { nativeOwnerDid: grant.ownerDid }
2599
+ }
2600
+ });
2601
+ }
1923
2602
  if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
1924
2603
  const originFailure = validateOrigin(request, config.allowedOrigins);
1925
2604
  if (originFailure !== void 0) return originFailure;
@@ -2031,9 +2710,77 @@ function callbackCors(response, openkeyHost) {
2031
2710
  headers.set("vary", "Origin");
2032
2711
  return new Response(response.body, { status: response.status, headers });
2033
2712
  }
2713
+ async function readBoundedBodyText(request, maxBytes) {
2714
+ const contentLength = request.headers.get("content-length");
2715
+ if (contentLength !== null) {
2716
+ const parsedLength = Number(contentLength);
2717
+ if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
2718
+ }
2719
+ if (request.body === null) return "";
2720
+ const reader = request.body.getReader();
2721
+ const chunks = [];
2722
+ let total = 0;
2723
+ try {
2724
+ while (true) {
2725
+ const { done, value } = await reader.read();
2726
+ if (done) break;
2727
+ total += value.byteLength;
2728
+ if (total > maxBytes) {
2729
+ await reader.cancel();
2730
+ return void 0;
2731
+ }
2732
+ chunks.push(value);
2733
+ }
2734
+ } finally {
2735
+ reader.releaseLock();
2736
+ }
2737
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
2738
+ }
2739
+ function nativeGrantErrorResponse(error) {
2740
+ if (!(error instanceof NativeGrantValidationError)) {
2741
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2742
+ }
2743
+ const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
2744
+ return json({ error: error.code }, unavailable ? 503 : 401);
2745
+ }
2034
2746
  function json(value, status = 200) {
2035
2747
  return Response.json(value, { status, headers: { "cache-control": "no-store" } });
2036
2748
  }
2749
+ async function withBearerMethodsSupported(response) {
2750
+ if (response.status === 204 || response.body === null) return response;
2751
+ const metadata = await response.json();
2752
+ const headers = new Headers(response.headers);
2753
+ headers.delete("content-length");
2754
+ headers.delete("content-encoding");
2755
+ return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
2756
+ status: response.status,
2757
+ statusText: response.statusText,
2758
+ headers
2759
+ });
2760
+ }
2761
+ function publicMetadataCors(response, request) {
2762
+ const headers = new Headers(response.headers);
2763
+ headers.set("access-control-allow-origin", "*");
2764
+ headers.set("access-control-allow-methods", "GET, OPTIONS");
2765
+ const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
2766
+ if (requestedHeaders !== null) {
2767
+ headers.set("access-control-allow-headers", requestedHeaders);
2768
+ }
2769
+ headers.set(
2770
+ "vary",
2771
+ requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
2772
+ );
2773
+ return new Response(response.body, { status: response.status, headers });
2774
+ }
2775
+ function publicText(text, contentType) {
2776
+ return new Response(text, {
2777
+ headers: {
2778
+ "content-type": contentType,
2779
+ "cache-control": "public, max-age=300",
2780
+ "x-content-type-options": "nosniff"
2781
+ }
2782
+ });
2783
+ }
2037
2784
  function methodNotAllowed(allow) {
2038
2785
  return new Response("Method not allowed", { status: 405, headers: { allow } });
2039
2786
  }