@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/http.cjs CHANGED
@@ -272,16 +272,85 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
272
272
  }
273
273
 
274
274
  // src/remote-store.ts
275
- var import_node_crypto2 = require("crypto");
275
+ var import_node_crypto3 = require("crypto");
276
276
  var import_promises = require("fs/promises");
277
277
  var import_node_path = require("path");
278
278
  var import_node_sdk2 = require("@tinycloud/node-sdk");
279
279
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
280
+ var import_artifacts = require("@tinycloud/operations/artifacts");
281
+ var import_state = require("@tinycloud/operations/state");
280
282
  var import_operations = require("@tinycloud/operations");
281
283
  var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
282
284
  var import_tc_apps = require("@tinycloud/operations/tc-apps");
285
+
286
+ // src/native-auth.ts
287
+ var import_node_crypto2 = require("crypto");
288
+ var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
289
+ var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
290
+ var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
291
+ var NATIVE_GRANT_REQUIRED = "native_grant_required";
292
+ var NATIVE_GRANT_EXPIRED = "native_grant_expired";
293
+ var NATIVE_GRANT_REJECTED = "native_grant_rejected";
294
+ var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
295
+ var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
296
+ var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
297
+ var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
298
+ function serviceCredentialMatches(supplied, configured) {
299
+ if (supplied === null || configured === void 0) return false;
300
+ const suppliedBytes = Buffer.from(supplied, "utf8");
301
+ const configuredBytes = Buffer.from(configured, "utf8");
302
+ return suppliedBytes.byteLength === configuredBytes.byteLength && (0, import_node_crypto2.timingSafeEqual)(suppliedBytes, configuredBytes);
303
+ }
304
+ var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
305
+ var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
306
+ var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
307
+ function canonicalOwnerDid(value) {
308
+ const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
309
+ if (!match) return void 0;
310
+ const chainId = Number(match[1]);
311
+ if (!Number.isSafeInteger(chainId)) return void 0;
312
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
313
+ }
314
+ function nativeOwnerForConnectorGrant(permissions) {
315
+ if (permissions.length !== 2) return void 0;
316
+ const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
317
+ const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
318
+ if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
319
+ const sql = sqlEntries[0];
320
+ const kv = kvEntries[0];
321
+ if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
322
+ if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
323
+ if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
324
+ const sqlSpace = ownerFromApplicationsSpace(sql.space);
325
+ const kvSpace = ownerFromApplicationsSpace(kv.space);
326
+ return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
327
+ }
328
+ function ownerFromApplicationsSpace(space) {
329
+ const match = OWNER_APPLICATIONS_SPACE.exec(space);
330
+ if (!match) return void 0;
331
+ const chainId = Number(match[1]);
332
+ if (!Number.isSafeInteger(chainId)) return void 0;
333
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
334
+ }
335
+ function actionsEqual(actual, expected) {
336
+ const actualActions = [...actual].sort();
337
+ const expectedActions = [...expected].sort();
338
+ return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
339
+ }
340
+ function hasCaveats(permission) {
341
+ return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
342
+ }
343
+
344
+ // src/remote-store.ts
283
345
  var PROFILE = "agent";
284
346
  var MAX_CALLBACK_BYTES = 256 * 1024;
347
+ var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
348
+ "DELEGATION_ARTIFACT_INVALID",
349
+ "DELEGATION_EXPIRED",
350
+ "DELEGATION_AUDIENCE_MISMATCH",
351
+ "DELEGATION_HOST_MISMATCH",
352
+ "DELEGATION_REJECTED"
353
+ ]);
285
354
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
286
355
  var BOOTSTRAP_PERMISSIONS = [
287
356
  {
@@ -323,6 +392,15 @@ var ApprovalRequestError = class extends Error {
323
392
  this.detail = detail;
324
393
  }
325
394
  };
395
+ var NativeGrantValidationError = class extends Error {
396
+ code;
397
+ constructor(status) {
398
+ 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";
399
+ super(code);
400
+ this.name = "NativeGrantValidationError";
401
+ this.code = code;
402
+ }
403
+ };
326
404
  var RemoteTenantStore = class {
327
405
  #config;
328
406
  #initializing = /* @__PURE__ */ new Map();
@@ -335,9 +413,186 @@ var RemoteTenantStore = class {
335
413
  #scopedClearing = /* @__PURE__ */ new Map();
336
414
  #clearedScoped = /* @__PURE__ */ new Map();
337
415
  #approvalProcessing = /* @__PURE__ */ new Map();
416
+ #nativeIdentity;
417
+ #nativeGrantQueues = /* @__PURE__ */ new Map();
338
418
  constructor(config) {
339
419
  this.#config = config;
340
420
  }
421
+ async nativeServiceIdentity() {
422
+ const identity = await this.#loadNativeServiceKey();
423
+ return { serviceDid: normalizeDid(identity.did) };
424
+ }
425
+ nativeTenantStateRoot(ownerDid) {
426
+ const canonical = canonicalOwnerDid(ownerDid);
427
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
428
+ const tenant = (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
429
+ return (0, import_node_path.join)(this.#config.stateDir, "native-tenants", tenant);
430
+ }
431
+ async acceptNativeGrant(serializedDelegation) {
432
+ if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
433
+ throw new NativeGrantValidationError("rejected");
434
+ }
435
+ const identity = await this.#loadNativeServiceKey();
436
+ const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
437
+ const { ownerDid, spaceId, permissions, expiresAt } = activated;
438
+ return this.#withNativeGrantLock(ownerDid, async () => {
439
+ const stateRoot = this.nativeTenantStateRoot(ownerDid);
440
+ let previous = null;
441
+ try {
442
+ const stored = await readJson(this.#nativeGrantPath(stateRoot));
443
+ 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))) {
444
+ previous = stored;
445
+ }
446
+ } catch (error) {
447
+ if (!(error instanceof SyntaxError)) throw error;
448
+ }
449
+ if (previous !== null) {
450
+ const previousExpiry = Date.parse(previous.expiresAt);
451
+ const nextExpiry = expiresAt.getTime();
452
+ if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
453
+ throw new NativeGrantValidationError("rejected");
454
+ }
455
+ if (previous.delegationCid === activated.cid) {
456
+ return {
457
+ ownerDid,
458
+ serviceDid: normalizeDid(identity.did),
459
+ expiresAt: previous.expiresAt,
460
+ stateRoot
461
+ };
462
+ }
463
+ }
464
+ const session = {
465
+ delegationHeader: activated.delegation.delegationHeader,
466
+ delegationCid: activated.cid,
467
+ spaceId,
468
+ jwk: identity.jwk,
469
+ verificationMethod: identity.did,
470
+ tinycloudHosts: [this.#config.nodeHost]
471
+ };
472
+ const grant = {
473
+ version: 1,
474
+ ownerDid,
475
+ serviceDid: normalizeDid(identity.did),
476
+ serializedDelegation: (0, import_node_sdk2.serializeDelegation)(activated.delegation),
477
+ delegationCid: activated.cid,
478
+ spaceId,
479
+ permissions,
480
+ expiresAt: expiresAt.toISOString()
481
+ };
482
+ const sessionPath = this.#sessionPath(stateRoot);
483
+ const previousSession = await readOptionalText(sessionPath);
484
+ try {
485
+ await secureWrite(this.#profilePath(stateRoot), {
486
+ name: PROFILE,
487
+ host: this.#config.nodeHost,
488
+ chainId: activated.delegation.chainId,
489
+ spaceName: "applications",
490
+ did: identity.did,
491
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
492
+ profile: PROFILE,
493
+ operatorType: "agent",
494
+ posture: "delegate-session",
495
+ ownerDid,
496
+ sessionDid: identity.did,
497
+ spaceId,
498
+ tinycloudHost: this.#config.nodeHost
499
+ });
500
+ await secureWrite(this.#keyPath(stateRoot), identity.jwk);
501
+ await secureWrite(sessionPath, session);
502
+ const request = await (0, import_state.withTinyCloudStateRoot)(stateRoot, () => (0, import_artifacts.createOrReusePermissionRequest)({
503
+ profile: PROFILE,
504
+ posture: "delegate-session",
505
+ operatorType: "agent",
506
+ host: this.#config.nodeHost,
507
+ sessionDid: identity.did,
508
+ ownerDid,
509
+ spaceId,
510
+ missing: permissions,
511
+ granted: [],
512
+ replace: true
513
+ }));
514
+ if (request.status !== "created") {
515
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
516
+ }
517
+ const imported = await (0, import_operations.invokeOperation)(
518
+ "tinycloud.auth.import",
519
+ 1,
520
+ { profile: PROFILE, stateRoot },
521
+ {
522
+ kind: "tinycloud.auth.delegation",
523
+ version: 1,
524
+ requestId: request.request.requestId,
525
+ delegationCid: activated.cid,
526
+ delegation: activated.delegation
527
+ }
528
+ );
529
+ if (imported.status === "error") {
530
+ throw new NativeGrantValidationError(
531
+ NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
532
+ );
533
+ }
534
+ if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
535
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
536
+ }
537
+ await secureWrite(this.#nativeGrantPath(stateRoot), grant);
538
+ } catch (error) {
539
+ try {
540
+ await restoreOptionalText(sessionPath, previousSession);
541
+ } catch {
542
+ await (0, import_promises.rm)(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
543
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
544
+ }
545
+ throw error;
546
+ }
547
+ return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
548
+ });
549
+ }
550
+ async nativeGrantForOwner(ownerDid) {
551
+ const canonical = canonicalOwnerDid(ownerDid);
552
+ if (canonical === void 0) return { status: "rejected" };
553
+ const stateRoot = this.nativeTenantStateRoot(canonical);
554
+ let stored;
555
+ try {
556
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
557
+ } catch {
558
+ return { status: "rejected" };
559
+ }
560
+ if (stored === null) return { status: "required" };
561
+ 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) {
562
+ return { status: "rejected" };
563
+ }
564
+ const expires = Date.parse(stored.expiresAt);
565
+ if (!Number.isFinite(expires)) return { status: "rejected" };
566
+ if (expires <= Date.now()) return { status: "expired" };
567
+ try {
568
+ const identity = await this.#loadNativeServiceKey();
569
+ if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
570
+ const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
571
+ 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" };
572
+ return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
573
+ } catch (error) {
574
+ if (error instanceof NativeGrantValidationError) {
575
+ return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
576
+ }
577
+ return { status: "unavailable" };
578
+ }
579
+ }
580
+ async assertNativeGrantActive(ownerDid) {
581
+ const canonical = canonicalOwnerDid(ownerDid);
582
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
583
+ const stateRoot = this.nativeTenantStateRoot(canonical);
584
+ let stored;
585
+ try {
586
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
587
+ } catch {
588
+ throw new NativeGrantValidationError("rejected");
589
+ }
590
+ if (stored === null) throw new NativeGrantValidationError("required");
591
+ if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
592
+ throw new NativeGrantValidationError("rejected");
593
+ }
594
+ if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
595
+ }
341
596
  tenantStateRoot(subject) {
342
597
  return (0, import_node_path.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
343
598
  }
@@ -864,7 +1119,7 @@ var RemoteTenantStore = class {
864
1119
  { tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
865
1120
  );
866
1121
  if (reusable !== void 0) return reusable;
867
- const nonce = (0, import_node_crypto2.randomBytes)(24).toString("base64url");
1122
+ const nonce = (0, import_node_crypto3.randomBytes)(24).toString("base64url");
868
1123
  const created = Date.now();
869
1124
  const pending = {
870
1125
  version: 1,
@@ -889,7 +1144,7 @@ var RemoteTenantStore = class {
889
1144
  const expected = this.#signState(tenantId, nonce);
890
1145
  const supplied = Buffer.from(signature, "base64url");
891
1146
  const expectedBytes = Buffer.from(expected, "base64url");
892
- if (supplied.length !== expectedBytes.length || !(0, import_node_crypto2.timingSafeEqual)(supplied, expectedBytes)) {
1147
+ if (supplied.length !== expectedBytes.length || !(0, import_node_crypto3.timingSafeEqual)(supplied, expectedBytes)) {
893
1148
  throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
894
1149
  }
895
1150
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
@@ -940,11 +1195,128 @@ var RemoteTenantStore = class {
940
1195
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
941
1196
  return url.toString();
942
1197
  }
1198
+ async #withNativeGrantLock(ownerDid, action) {
1199
+ const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
1200
+ let releaseNext;
1201
+ const next = new Promise((resolve) => {
1202
+ releaseNext = resolve;
1203
+ });
1204
+ this.#nativeGrantQueues.set(ownerDid, next);
1205
+ await previous;
1206
+ try {
1207
+ return await action();
1208
+ } finally {
1209
+ releaseNext();
1210
+ if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
1211
+ }
1212
+ }
1213
+ async #loadNativeServiceKey() {
1214
+ if (this.#nativeIdentity === void 0) {
1215
+ const loading = this.#readOrCreateNativeServiceKey();
1216
+ this.#nativeIdentity = loading;
1217
+ void loading.catch(() => {
1218
+ if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
1219
+ });
1220
+ }
1221
+ return this.#nativeIdentity;
1222
+ }
1223
+ async #readOrCreateNativeServiceKey() {
1224
+ const path = (0, import_node_path.join)(this.#config.stateDir, ".tinycloud", "native-service-key.json");
1225
+ await secureDirectory((0, import_node_path.dirname)(path));
1226
+ let stored = await readJson(path);
1227
+ if (stored === null) {
1228
+ const generated = generateSessionKey();
1229
+ try {
1230
+ await (0, import_promises.writeFile)(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
1231
+ `, {
1232
+ mode: 384,
1233
+ flag: "wx"
1234
+ });
1235
+ } catch (error) {
1236
+ if (!isRecord(error) || error.code !== "EEXIST") throw error;
1237
+ }
1238
+ await (0, import_promises.chmod)(path, 384);
1239
+ stored = await readJson(path);
1240
+ }
1241
+ if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
1242
+ throw new Error("The native MCP service identity is unavailable.");
1243
+ }
1244
+ return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
1245
+ }
1246
+ async #activateNativeDelegation(serialized, identity) {
1247
+ let delegation;
1248
+ try {
1249
+ const parsed = JSON.parse(serialized);
1250
+ if (!isRecord(parsed)) throw new Error("invalid delegation");
1251
+ delegation = (0, import_node_sdk2.deserializeDelegation)(serialized);
1252
+ } catch {
1253
+ throw new NativeGrantValidationError("native_grant_rejected");
1254
+ }
1255
+ 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())) {
1256
+ throw new NativeGrantValidationError("native_grant_rejected");
1257
+ }
1258
+ const now = Date.now();
1259
+ const claimedExpiry = delegation.expiry.getTime();
1260
+ if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
1261
+ if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1262
+ throw new NativeGrantValidationError("native_grant_rejected");
1263
+ }
1264
+ let ownerDid;
1265
+ let spaceId;
1266
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
1267
+ try {
1268
+ await node.restoreSession({
1269
+ delegationHeader: delegation.delegationHeader,
1270
+ delegationCid: delegation.cid,
1271
+ spaceId: delegation.spaceId,
1272
+ jwk: identity.jwk,
1273
+ verificationMethod: identity.did,
1274
+ tinycloudHosts: [this.#config.nodeHost]
1275
+ });
1276
+ const activated = await (0, import_node_sdk2.activateValidatedRuntimeDelegation)(node, delegation, {
1277
+ host: this.#config.nodeHost,
1278
+ authorize: (permissions) => {
1279
+ const derivedOwner = nativeOwnerForConnectorGrant(permissions);
1280
+ const signedSpace = permissions[0]?.space;
1281
+ if (derivedOwner === void 0 || signedSpace === void 0) return false;
1282
+ const metadataOwner = canonicalOwnerDid(
1283
+ `did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
1284
+ );
1285
+ if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
1286
+ ownerDid = derivedOwner;
1287
+ spaceId = signedSpace;
1288
+ return true;
1289
+ }
1290
+ });
1291
+ if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1292
+ throw new NativeGrantValidationError("native_grant_rejected");
1293
+ }
1294
+ return {
1295
+ ownerDid,
1296
+ spaceId,
1297
+ permissions: [...activated.effectivePermissions],
1298
+ cid: activated.cid,
1299
+ expiresAt: activated.expiry,
1300
+ delegation: activated.delegation
1301
+ };
1302
+ } catch (error) {
1303
+ if (error instanceof NativeGrantValidationError) throw error;
1304
+ if (error instanceof import_node_sdk2.RuntimeDelegationRejectedError) {
1305
+ throw new NativeGrantValidationError(
1306
+ error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
1307
+ );
1308
+ }
1309
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
1310
+ }
1311
+ }
1312
+ #nativeGrantPath(stateRoot) {
1313
+ return (0, import_node_path.join)(stateRoot, ".tinycloud", "native-grant.json");
1314
+ }
943
1315
  #tenantId(subject) {
944
- return (0, import_node_crypto2.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
1316
+ return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
945
1317
  }
946
1318
  #signState(tenantId, nonce) {
947
- return (0, import_node_crypto2.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
1319
+ return (0, import_node_crypto3.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
948
1320
  }
949
1321
  #profilePath(stateRoot) {
950
1322
  return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
@@ -1033,6 +1405,21 @@ function samePrincipal(left, right) {
1033
1405
  function normalizeDid(value) {
1034
1406
  return value.split("#", 1)[0];
1035
1407
  }
1408
+ function sameNodeHost(left, right) {
1409
+ try {
1410
+ const normalize = (value) => {
1411
+ const url = new URL(value);
1412
+ if (url.username !== "" || url.password !== "") return "";
1413
+ url.hash = "";
1414
+ url.search = "";
1415
+ url.pathname = url.pathname.replace(/\/+$/, "");
1416
+ return url.toString().replace(/\/+$/, "");
1417
+ };
1418
+ return normalize(left) !== "" && normalize(left) === normalize(right);
1419
+ } catch {
1420
+ return false;
1421
+ }
1422
+ }
1036
1423
  function isPermissionEntry(value) {
1037
1424
  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");
1038
1425
  }
@@ -1046,7 +1433,7 @@ async function secureDirectory(path) {
1046
1433
  async function secureWrite(path, value) {
1047
1434
  const directory = (0, import_node_path.dirname)(path);
1048
1435
  await secureDirectory(directory);
1049
- const temporary = `${path}.${process.pid}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
1436
+ const temporary = `${path}.${process.pid}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`;
1050
1437
  await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
1051
1438
  `, { mode: 384 });
1052
1439
  await (0, import_promises.rename)(temporary, path);
@@ -1063,6 +1450,23 @@ async function readJson(path) {
1063
1450
  throw error;
1064
1451
  }
1065
1452
  }
1453
+ async function readOptionalText(path) {
1454
+ try {
1455
+ return await (0, import_promises.readFile)(path, "utf8");
1456
+ } catch (error) {
1457
+ if (isRecord(error) && error.code === "ENOENT") return null;
1458
+ throw error;
1459
+ }
1460
+ }
1461
+ async function restoreOptionalText(path, contents) {
1462
+ if (contents === null) {
1463
+ await (0, import_promises.rm)(path, { force: true });
1464
+ return;
1465
+ }
1466
+ await secureDirectory((0, import_node_path.dirname)(path));
1467
+ await (0, import_promises.writeFile)(path, contents, { mode: 384 });
1468
+ await (0, import_promises.chmod)(path, 384);
1469
+ }
1066
1470
  async function reusablePending(directory, expected) {
1067
1471
  let names;
1068
1472
  try {
@@ -1460,7 +1864,7 @@ function createJsonSchemaValidator() {
1460
1864
  return new import_ajv.AjvJsonSchemaValidator();
1461
1865
  }
1462
1866
  function registerTinyCloudTools(server, selection, validator) {
1463
- const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
1867
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
1464
1868
  for (const binding of bindings) {
1465
1869
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
1466
1870
  if (operation === void 0) {
@@ -1538,7 +1942,7 @@ function outcomeCode(result) {
1538
1942
  // package.json
1539
1943
  var package_default = {
1540
1944
  name: "@tinycloud/mcp",
1541
- version: "0.8.3-beta.9",
1945
+ version: "0.8.4-beta.0",
1542
1946
  description: "TinyCloud delegated operations MCP server",
1543
1947
  keywords: [
1544
1948
  "tinycloud",
@@ -1609,9 +2013,9 @@ var package_default = {
1609
2013
  dependencies: {
1610
2014
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1611
2015
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1612
- "@tinycloud/node-sdk": "3.8.0-beta.7",
2016
+ "@tinycloud/node-sdk": "3.8.1-beta.0",
1613
2017
  "@tinycloud/node-sdk-wasm": "1.7.8",
1614
- "@tinycloud/operations": "0.7.7-beta.8",
2018
+ "@tinycloud/operations": "0.7.8-beta.0",
1615
2019
  jose: "6.2.4"
1616
2020
  },
1617
2021
  devDependencies: {
@@ -1635,13 +2039,19 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
1635
2039
 
1636
2040
  // src/http.ts
1637
2041
  var REQUIRED_SCOPE = "tinycloud:mcp";
2042
+ var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
2043
+ var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
1638
2044
  function createHostedMcpApp(config) {
2045
+ if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
2046
+ throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
2047
+ }
1639
2048
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1640
2049
  const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1641
2050
  const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
1642
2051
  const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
1643
2052
  const plugin = createHostedSurface({
1644
2053
  ...config,
2054
+ nativeServiceCredential: void 0,
1645
2055
  stateDir: (0, import_node_path3.join)(config.stateDir, "chatgpt"),
1646
2056
  approvalPath: new URL("./connect", pluginUrl).pathname
1647
2057
  }, pluginUrl, "public", statusFor(pluginUrl));
@@ -1677,16 +2087,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1677
2087
  resourceMetadataUrl
1678
2088
  });
1679
2089
  const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
2090
+ const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
2091
+ if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
1680
2092
  const handler = (0, import_server4.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
1681
- const identity = oauthIdentity(authInfo2);
2093
+ const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
2094
+ const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
1682
2095
  const { subject, ownerDids } = identity;
1683
- const stateRoot = await store.initializeTenant(subject);
2096
+ const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
1684
2097
  const validator = createJsonSchemaValidator();
1685
2098
  const server = new import_server4.McpServer(
1686
2099
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1687
2100
  {
1688
2101
  jsonSchemaValidator: validator,
1689
- 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."
2102
+ 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."
1690
2103
  }
1691
2104
  );
1692
2105
  registerTinyCloudTools(server, {
@@ -1695,10 +2108,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1695
2108
  explicitProfile: true,
1696
2109
  allowOwnerProfile: false,
1697
2110
  stateRoot,
1698
- beforeOperation: () => store.validateTenant(subject, ownerDids),
1699
- transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2111
+ ...nativeOwner === void 0 ? {} : {
2112
+ allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
2113
+ },
2114
+ beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
2115
+ ...nativeOwner === void 0 ? {
2116
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2117
+ } : {}
1700
2118
  }, validator);
1701
- registerConnectTool(server, validator, store, identity, surface);
2119
+ if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
1702
2120
  return server;
1703
2121
  }, {
1704
2122
  legacy: "stateless",
@@ -1714,14 +2132,205 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1714
2132
  const allowedHosts = [resourceUrl.hostname];
1715
2133
  return {
1716
2134
  async fetch(request) {
1717
- const metadata = (0, import_server4.oauthMetadataResponse)(request, metadataOptions);
1718
- if (metadata !== void 0) return metadata;
1719
2135
  const url = new URL(request.url);
2136
+ if (url.pathname !== "/healthz") {
2137
+ const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
2138
+ if (hostFailure !== void 0) return hostFailure;
2139
+ }
2140
+ if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
2141
+ const metadataUrl = new URL((0, import_server4.getOAuthProtectedResourceMetadataUrl)(resourceUrl));
2142
+ const metadataRequest = new Request(metadataUrl.toString(), {
2143
+ method: request.method,
2144
+ headers: request.headers
2145
+ });
2146
+ const response = (0, import_server4.oauthMetadataResponse)(metadataRequest, metadataOptions);
2147
+ if (response !== void 0) return withBearerMethodsSupported(response);
2148
+ }
2149
+ const metadata = (0, import_server4.oauthMetadataResponse)(request, metadataOptions);
2150
+ if (metadata !== void 0) {
2151
+ return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
2152
+ }
2153
+ if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
2154
+ if (request.method === "OPTIONS") {
2155
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2156
+ }
2157
+ if (request.method !== "GET") return methodNotAllowed("GET");
2158
+ return publicMetadataCors(Response.json({
2159
+ specVersion: "1.0",
2160
+ host: {
2161
+ displayName: "TinyCloud, Inc.",
2162
+ identifier: "https://mcp.tinycloud.xyz",
2163
+ documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
2164
+ },
2165
+ entries: [{
2166
+ identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
2167
+ displayName: "TinyCloud MCP",
2168
+ type: "application/json",
2169
+ description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
2170
+ url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
2171
+ representativeQueries: [
2172
+ "How do I connect to TinyCloud's hosted MCP server?",
2173
+ "Which endpoint provides TinyCloud's delegated MCP operations?",
2174
+ "How can I connect to TinyCloud MCP using local stdio?"
2175
+ ]
2176
+ }]
2177
+ }, {
2178
+ headers: {
2179
+ "cache-control": "public, max-age=300",
2180
+ "content-type": "application/json; charset=utf-8"
2181
+ }
2182
+ }), request);
2183
+ }
2184
+ if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
2185
+ if (request.method === "OPTIONS") {
2186
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2187
+ }
2188
+ if (request.method !== "GET") return methodNotAllowed("GET");
2189
+ return publicMetadataCors(Response.json({
2190
+ protocol_version: "0.1",
2191
+ organization: {
2192
+ name: "TinyCloud, Inc.",
2193
+ domain: resourceUrl.hostname
2194
+ },
2195
+ web: {}
2196
+ }, {
2197
+ headers: {
2198
+ "cache-control": "public, max-age=300",
2199
+ "content-type": "application/json; charset=utf-8"
2200
+ }
2201
+ }), request);
2202
+ }
2203
+ if (url.pathname === "/auth.md" && surface === "hosted") {
2204
+ if (request.method === "OPTIONS") {
2205
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2206
+ }
2207
+ if (request.method !== "GET") return methodNotAllowed("GET");
2208
+ const issuerUrl = new URL(issuer);
2209
+ const authorizationServerMetadataUrl = new URL(
2210
+ `/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
2211
+ issuerUrl.origin
2212
+ );
2213
+ const registrationEndpoint = config.oauthMetadata.registration_endpoint;
2214
+ 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.";
2215
+ const authDoc = [
2216
+ "# TinyCloud MCP auth.md",
2217
+ "",
2218
+ `Protected resource: ${resourceUrl}.`,
2219
+ "",
2220
+ "Send an OAuth access token in the `Authorization: Bearer` header.",
2221
+ "",
2222
+ `Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
2223
+ `Authorization server issuer: ${issuer}.`,
2224
+ `Authorization server metadata: ${authorizationServerMetadataUrl}.`,
2225
+ `Scope: \`${REQUIRED_SCOPE}\`.`,
2226
+ "",
2227
+ "Hosted access uses OpenKey OAuth.",
2228
+ registrationNote,
2229
+ "TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
2230
+ "No `agent_auth` registration block is published because no agent-registration endpoint is available.",
2231
+ "",
2232
+ `Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
2233
+ "",
2234
+ "Connection steps: https://docs.tinycloud.xyz/cli/mcp"
2235
+ ].join("\n") + "\n";
2236
+ return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
2237
+ }
2238
+ if (url.pathname === "/robots.txt" && surface === "hosted") {
2239
+ return request.method === "GET" ? publicText(`User-agent: *
2240
+ Allow: /
2241
+ Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
2242
+ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
2243
+ `, "text/plain; charset=utf-8") : methodNotAllowed("GET");
2244
+ }
2245
+ if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
2246
+ if (request.method === "OPTIONS") {
2247
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2248
+ }
2249
+ if (request.method !== "GET") return methodNotAllowed("GET");
2250
+ const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
2251
+ const card = {
2252
+ $schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
2253
+ name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
2254
+ title: "TinyCloud MCP",
2255
+ version: MCP_VERSION,
2256
+ description: "MCP server for delegated TinyCloud operations.",
2257
+ websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
2258
+ repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
2259
+ remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
2260
+ };
2261
+ if (identity !== void 0) {
2262
+ card["x-tinycloud-native-auth"] = {
2263
+ serviceDid: identity.serviceDid,
2264
+ serviceDidField: "x-tinycloud-native-auth.serviceDid",
2265
+ grantEndpoint: {
2266
+ method: "POST",
2267
+ path: "/native/grant",
2268
+ body: { delegation: "serialized PortableDelegation JSON string" }
2269
+ },
2270
+ serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
2271
+ serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
2272
+ ownerDidHeader: NATIVE_OWNER_DID_HEADER,
2273
+ maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
2274
+ acceptedCapabilities: [
2275
+ { service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
2276
+ { service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
2277
+ ],
2278
+ tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
2279
+ errors: {
2280
+ required: NATIVE_GRANT_REQUIRED,
2281
+ expired: NATIVE_GRANT_EXPIRED,
2282
+ rejected: NATIVE_GRANT_REJECTED,
2283
+ serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
2284
+ serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
2285
+ validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
2286
+ }
2287
+ };
2288
+ }
2289
+ return publicMetadataCors(Response.json(card, {
2290
+ headers: { "cache-control": "public, max-age=300" }
2291
+ }), request);
2292
+ }
2293
+ if (url.pathname === "/llms.txt" && surface === "hosted") {
2294
+ return request.method === "GET" ? publicText(`# TinyCloud MCP
2295
+
2296
+ Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
2297
+
2298
+ Connection documentation: https://docs.tinycloud.xyz/cli/mcp
2299
+
2300
+ Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
2301
+ `, "text/plain; charset=utf-8") : methodNotAllowed("GET");
2302
+ }
2303
+ if (url.pathname === "/sitemap.xml" && surface === "hosted") {
2304
+ 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>`;
2305
+ return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
2306
+ }
2307
+ if (url.pathname === "/" && surface === "hosted") {
2308
+ if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
2309
+ const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
2310
+ const content = markdown ? `# TinyCloud MCP
2311
+
2312
+ Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
2313
+
2314
+ [Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
2315
+
2316
+ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
2317
+ ` : `<!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>`;
2318
+ const headers = new Headers({
2319
+ "content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
2320
+ "cache-control": "public, max-age=300",
2321
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
2322
+ "x-content-type-options": "nosniff",
2323
+ vary: "Accept"
2324
+ });
2325
+ headers.set(
2326
+ "link",
2327
+ `<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
2328
+ );
2329
+ return new Response(request.method === "HEAD" ? null : content, { headers });
2330
+ }
1720
2331
  if (url.pathname === "/healthz") {
1721
2332
  return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
1722
2333
  }
1723
- const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
1724
- if (hostFailure !== void 0) return hostFailure;
1725
2334
  if (url.pathname === connectPath) {
1726
2335
  if (request.method !== "GET") return methodNotAllowed("GET");
1727
2336
  try {
@@ -1777,6 +2386,73 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1777
2386
  );
1778
2387
  }
1779
2388
  }
2389
+ if (url.pathname === "/native/grant" && surface === "hosted") {
2390
+ if (request.method !== "POST") return methodNotAllowed("POST");
2391
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2392
+ if (config.nativeServiceCredential === void 0) {
2393
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2394
+ }
2395
+ if (!serviceCredentialMatches(
2396
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2397
+ config.nativeServiceCredential
2398
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2399
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
2400
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2401
+ }
2402
+ const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
2403
+ if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
2404
+ let body;
2405
+ try {
2406
+ body = JSON.parse(bodyText);
2407
+ } catch {
2408
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2409
+ }
2410
+ if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
2411
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2412
+ }
2413
+ const grantBody = body;
2414
+ if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
2415
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2416
+ }
2417
+ try {
2418
+ const accepted = await store.acceptNativeGrant(grantBody.delegation);
2419
+ return json({
2420
+ ownerDid: accepted.ownerDid,
2421
+ serviceDid: accepted.serviceDid,
2422
+ expiresAt: accepted.expiresAt
2423
+ }, 201);
2424
+ } catch (error) {
2425
+ return nativeGrantErrorResponse(error);
2426
+ }
2427
+ }
2428
+ if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
2429
+ if (config.nativeServiceCredential === void 0) {
2430
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2431
+ }
2432
+ if (!serviceCredentialMatches(
2433
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2434
+ config.nativeServiceCredential
2435
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2436
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2437
+ const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
2438
+ if (ownerHeader === null || ownerHeader.length === 0) {
2439
+ return json({ error: NATIVE_GRANT_REQUIRED }, 401);
2440
+ }
2441
+ const grant = await store.nativeGrantForOwner(ownerHeader);
2442
+ if (grant.status !== "ok") {
2443
+ const status = grant.status === "unavailable" ? 503 : 401;
2444
+ const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
2445
+ return json({ error: code }, status);
2446
+ }
2447
+ return handler.fetch(request, {
2448
+ authInfo: {
2449
+ token: "native-service",
2450
+ clientId: "tinycloud-native",
2451
+ scopes: [],
2452
+ extra: { nativeOwnerDid: grant.ownerDid }
2453
+ }
2454
+ });
2455
+ }
1780
2456
  if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
1781
2457
  const originFailure = validateOrigin(request, config.allowedOrigins);
1782
2458
  if (originFailure !== void 0) return originFailure;
@@ -1888,9 +2564,77 @@ function callbackCors(response, openkeyHost) {
1888
2564
  headers.set("vary", "Origin");
1889
2565
  return new Response(response.body, { status: response.status, headers });
1890
2566
  }
2567
+ async function readBoundedBodyText(request, maxBytes) {
2568
+ const contentLength = request.headers.get("content-length");
2569
+ if (contentLength !== null) {
2570
+ const parsedLength = Number(contentLength);
2571
+ if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
2572
+ }
2573
+ if (request.body === null) return "";
2574
+ const reader = request.body.getReader();
2575
+ const chunks = [];
2576
+ let total = 0;
2577
+ try {
2578
+ while (true) {
2579
+ const { done, value } = await reader.read();
2580
+ if (done) break;
2581
+ total += value.byteLength;
2582
+ if (total > maxBytes) {
2583
+ await reader.cancel();
2584
+ return void 0;
2585
+ }
2586
+ chunks.push(value);
2587
+ }
2588
+ } finally {
2589
+ reader.releaseLock();
2590
+ }
2591
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
2592
+ }
2593
+ function nativeGrantErrorResponse(error) {
2594
+ if (!(error instanceof NativeGrantValidationError)) {
2595
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2596
+ }
2597
+ const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
2598
+ return json({ error: error.code }, unavailable ? 503 : 401);
2599
+ }
1891
2600
  function json(value, status = 200) {
1892
2601
  return Response.json(value, { status, headers: { "cache-control": "no-store" } });
1893
2602
  }
2603
+ async function withBearerMethodsSupported(response) {
2604
+ if (response.status === 204 || response.body === null) return response;
2605
+ const metadata = await response.json();
2606
+ const headers = new Headers(response.headers);
2607
+ headers.delete("content-length");
2608
+ headers.delete("content-encoding");
2609
+ return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
2610
+ status: response.status,
2611
+ statusText: response.statusText,
2612
+ headers
2613
+ });
2614
+ }
2615
+ function publicMetadataCors(response, request) {
2616
+ const headers = new Headers(response.headers);
2617
+ headers.set("access-control-allow-origin", "*");
2618
+ headers.set("access-control-allow-methods", "GET, OPTIONS");
2619
+ const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
2620
+ if (requestedHeaders !== null) {
2621
+ headers.set("access-control-allow-headers", requestedHeaders);
2622
+ }
2623
+ headers.set(
2624
+ "vary",
2625
+ requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
2626
+ );
2627
+ return new Response(response.body, { status: response.status, headers });
2628
+ }
2629
+ function publicText(text, contentType) {
2630
+ return new Response(text, {
2631
+ headers: {
2632
+ "content-type": contentType,
2633
+ "cache-control": "public, max-age=300",
2634
+ "x-content-type-options": "nosniff"
2635
+ }
2636
+ });
2637
+ }
1894
2638
  function methodNotAllowed(allow) {
1895
2639
  return new Response("Method not allowed", { status: 405, headers: { allow } });
1896
2640
  }