@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-cli.js CHANGED
@@ -276,12 +276,15 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
276
276
  }
277
277
 
278
278
  // src/remote-store.ts
279
- import { createHmac, randomBytes, timingSafeEqual } from "crypto";
279
+ import { createHmac, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
280
280
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
281
281
  import { dirname, join } from "path";
282
282
  import {
283
+ activateValidatedRuntimeDelegation,
284
+ deserializeDelegation,
283
285
  InvalidRestoredSessionError,
284
286
  RuntimeDelegationRejectedError,
287
+ serializeDelegation,
285
288
  TinyCloudNode,
286
289
  UnsupportedSessionRestoreError,
287
290
  isCapabilitySubset,
@@ -290,11 +293,80 @@ import {
290
293
  signedCompactUcanExpiry
291
294
  } from "@tinycloud/node-sdk";
292
295
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
296
+ import { createOrReusePermissionRequest } from "@tinycloud/operations/artifacts";
297
+ import { withTinyCloudStateRoot } from "@tinycloud/operations/state";
293
298
  import { invokeOperation } from "@tinycloud/operations";
294
299
  import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
295
300
  import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
301
+
302
+ // src/native-auth.ts
303
+ import { timingSafeEqual } from "crypto";
304
+ var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
305
+ var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
306
+ var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
307
+ var NATIVE_GRANT_REQUIRED = "native_grant_required";
308
+ var NATIVE_GRANT_EXPIRED = "native_grant_expired";
309
+ var NATIVE_GRANT_REJECTED = "native_grant_rejected";
310
+ var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
311
+ var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
312
+ var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
313
+ var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
314
+ function serviceCredentialMatches(supplied, configured) {
315
+ if (supplied === null || configured === void 0) return false;
316
+ const suppliedBytes = Buffer.from(supplied, "utf8");
317
+ const configuredBytes = Buffer.from(configured, "utf8");
318
+ return suppliedBytes.byteLength === configuredBytes.byteLength && timingSafeEqual(suppliedBytes, configuredBytes);
319
+ }
320
+ var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
321
+ var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
322
+ var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
323
+ function canonicalOwnerDid(value) {
324
+ const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
325
+ if (!match) return void 0;
326
+ const chainId = Number(match[1]);
327
+ if (!Number.isSafeInteger(chainId)) return void 0;
328
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
329
+ }
330
+ function nativeOwnerForConnectorGrant(permissions) {
331
+ if (permissions.length !== 2) return void 0;
332
+ const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
333
+ const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
334
+ if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
335
+ const sql = sqlEntries[0];
336
+ const kv = kvEntries[0];
337
+ if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
338
+ if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
339
+ if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
340
+ const sqlSpace = ownerFromApplicationsSpace(sql.space);
341
+ const kvSpace = ownerFromApplicationsSpace(kv.space);
342
+ return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
343
+ }
344
+ function ownerFromApplicationsSpace(space) {
345
+ const match = OWNER_APPLICATIONS_SPACE.exec(space);
346
+ if (!match) return void 0;
347
+ const chainId = Number(match[1]);
348
+ if (!Number.isSafeInteger(chainId)) return void 0;
349
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
350
+ }
351
+ function actionsEqual(actual, expected) {
352
+ const actualActions = [...actual].sort();
353
+ const expectedActions = [...expected].sort();
354
+ return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
355
+ }
356
+ function hasCaveats(permission) {
357
+ return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
358
+ }
359
+
360
+ // src/remote-store.ts
296
361
  var PROFILE = "agent";
297
362
  var MAX_CALLBACK_BYTES = 256 * 1024;
363
+ var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
364
+ "DELEGATION_ARTIFACT_INVALID",
365
+ "DELEGATION_EXPIRED",
366
+ "DELEGATION_AUDIENCE_MISMATCH",
367
+ "DELEGATION_HOST_MISMATCH",
368
+ "DELEGATION_REJECTED"
369
+ ]);
298
370
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
299
371
  var BOOTSTRAP_PERMISSIONS = [
300
372
  {
@@ -336,6 +408,15 @@ var ApprovalRequestError = class extends Error {
336
408
  this.detail = detail;
337
409
  }
338
410
  };
411
+ var NativeGrantValidationError = class extends Error {
412
+ code;
413
+ constructor(status) {
414
+ 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";
415
+ super(code);
416
+ this.name = "NativeGrantValidationError";
417
+ this.code = code;
418
+ }
419
+ };
339
420
  var RemoteTenantStore = class {
340
421
  #config;
341
422
  #initializing = /* @__PURE__ */ new Map();
@@ -348,9 +429,186 @@ var RemoteTenantStore = class {
348
429
  #scopedClearing = /* @__PURE__ */ new Map();
349
430
  #clearedScoped = /* @__PURE__ */ new Map();
350
431
  #approvalProcessing = /* @__PURE__ */ new Map();
432
+ #nativeIdentity;
433
+ #nativeGrantQueues = /* @__PURE__ */ new Map();
351
434
  constructor(config) {
352
435
  this.#config = config;
353
436
  }
437
+ async nativeServiceIdentity() {
438
+ const identity = await this.#loadNativeServiceKey();
439
+ return { serviceDid: normalizeDid(identity.did) };
440
+ }
441
+ nativeTenantStateRoot(ownerDid) {
442
+ const canonical = canonicalOwnerDid(ownerDid);
443
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
444
+ const tenant = createHmac("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
445
+ return join(this.#config.stateDir, "native-tenants", tenant);
446
+ }
447
+ async acceptNativeGrant(serializedDelegation) {
448
+ if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
449
+ throw new NativeGrantValidationError("rejected");
450
+ }
451
+ const identity = await this.#loadNativeServiceKey();
452
+ const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
453
+ const { ownerDid, spaceId, permissions, expiresAt } = activated;
454
+ return this.#withNativeGrantLock(ownerDid, async () => {
455
+ const stateRoot = this.nativeTenantStateRoot(ownerDid);
456
+ let previous = null;
457
+ try {
458
+ const stored = await readJson(this.#nativeGrantPath(stateRoot));
459
+ 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))) {
460
+ previous = stored;
461
+ }
462
+ } catch (error) {
463
+ if (!(error instanceof SyntaxError)) throw error;
464
+ }
465
+ if (previous !== null) {
466
+ const previousExpiry = Date.parse(previous.expiresAt);
467
+ const nextExpiry = expiresAt.getTime();
468
+ if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
469
+ throw new NativeGrantValidationError("rejected");
470
+ }
471
+ if (previous.delegationCid === activated.cid) {
472
+ return {
473
+ ownerDid,
474
+ serviceDid: normalizeDid(identity.did),
475
+ expiresAt: previous.expiresAt,
476
+ stateRoot
477
+ };
478
+ }
479
+ }
480
+ const session = {
481
+ delegationHeader: activated.delegation.delegationHeader,
482
+ delegationCid: activated.cid,
483
+ spaceId,
484
+ jwk: identity.jwk,
485
+ verificationMethod: identity.did,
486
+ tinycloudHosts: [this.#config.nodeHost]
487
+ };
488
+ const grant = {
489
+ version: 1,
490
+ ownerDid,
491
+ serviceDid: normalizeDid(identity.did),
492
+ serializedDelegation: serializeDelegation(activated.delegation),
493
+ delegationCid: activated.cid,
494
+ spaceId,
495
+ permissions,
496
+ expiresAt: expiresAt.toISOString()
497
+ };
498
+ const sessionPath = this.#sessionPath(stateRoot);
499
+ const previousSession = await readOptionalText(sessionPath);
500
+ try {
501
+ await secureWrite(this.#profilePath(stateRoot), {
502
+ name: PROFILE,
503
+ host: this.#config.nodeHost,
504
+ chainId: activated.delegation.chainId,
505
+ spaceName: "applications",
506
+ did: identity.did,
507
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
508
+ profile: PROFILE,
509
+ operatorType: "agent",
510
+ posture: "delegate-session",
511
+ ownerDid,
512
+ sessionDid: identity.did,
513
+ spaceId,
514
+ tinycloudHost: this.#config.nodeHost
515
+ });
516
+ await secureWrite(this.#keyPath(stateRoot), identity.jwk);
517
+ await secureWrite(sessionPath, session);
518
+ const request = await withTinyCloudStateRoot(stateRoot, () => createOrReusePermissionRequest({
519
+ profile: PROFILE,
520
+ posture: "delegate-session",
521
+ operatorType: "agent",
522
+ host: this.#config.nodeHost,
523
+ sessionDid: identity.did,
524
+ ownerDid,
525
+ spaceId,
526
+ missing: permissions,
527
+ granted: [],
528
+ replace: true
529
+ }));
530
+ if (request.status !== "created") {
531
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
532
+ }
533
+ const imported = await invokeOperation(
534
+ "tinycloud.auth.import",
535
+ 1,
536
+ { profile: PROFILE, stateRoot },
537
+ {
538
+ kind: "tinycloud.auth.delegation",
539
+ version: 1,
540
+ requestId: request.request.requestId,
541
+ delegationCid: activated.cid,
542
+ delegation: activated.delegation
543
+ }
544
+ );
545
+ if (imported.status === "error") {
546
+ throw new NativeGrantValidationError(
547
+ NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
548
+ );
549
+ }
550
+ if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
551
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
552
+ }
553
+ await secureWrite(this.#nativeGrantPath(stateRoot), grant);
554
+ } catch (error) {
555
+ try {
556
+ await restoreOptionalText(sessionPath, previousSession);
557
+ } catch {
558
+ await rm(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
559
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
560
+ }
561
+ throw error;
562
+ }
563
+ return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
564
+ });
565
+ }
566
+ async nativeGrantForOwner(ownerDid) {
567
+ const canonical = canonicalOwnerDid(ownerDid);
568
+ if (canonical === void 0) return { status: "rejected" };
569
+ const stateRoot = this.nativeTenantStateRoot(canonical);
570
+ let stored;
571
+ try {
572
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
573
+ } catch {
574
+ return { status: "rejected" };
575
+ }
576
+ if (stored === null) return { status: "required" };
577
+ 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) {
578
+ return { status: "rejected" };
579
+ }
580
+ const expires = Date.parse(stored.expiresAt);
581
+ if (!Number.isFinite(expires)) return { status: "rejected" };
582
+ if (expires <= Date.now()) return { status: "expired" };
583
+ try {
584
+ const identity = await this.#loadNativeServiceKey();
585
+ if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
586
+ const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
587
+ 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" };
588
+ return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
589
+ } catch (error) {
590
+ if (error instanceof NativeGrantValidationError) {
591
+ return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
592
+ }
593
+ return { status: "unavailable" };
594
+ }
595
+ }
596
+ async assertNativeGrantActive(ownerDid) {
597
+ const canonical = canonicalOwnerDid(ownerDid);
598
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
599
+ const stateRoot = this.nativeTenantStateRoot(canonical);
600
+ let stored;
601
+ try {
602
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
603
+ } catch {
604
+ throw new NativeGrantValidationError("rejected");
605
+ }
606
+ if (stored === null) throw new NativeGrantValidationError("required");
607
+ if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
608
+ throw new NativeGrantValidationError("rejected");
609
+ }
610
+ if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
611
+ }
354
612
  tenantStateRoot(subject) {
355
613
  return join(this.#config.stateDir, "tenants", this.#tenantId(subject));
356
614
  }
@@ -902,7 +1160,7 @@ var RemoteTenantStore = class {
902
1160
  const expected = this.#signState(tenantId, nonce);
903
1161
  const supplied = Buffer.from(signature, "base64url");
904
1162
  const expectedBytes = Buffer.from(expected, "base64url");
905
- if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
1163
+ if (supplied.length !== expectedBytes.length || !timingSafeEqual2(supplied, expectedBytes)) {
906
1164
  throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
907
1165
  }
908
1166
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
@@ -953,6 +1211,123 @@ var RemoteTenantStore = class {
953
1211
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
954
1212
  return url.toString();
955
1213
  }
1214
+ async #withNativeGrantLock(ownerDid, action) {
1215
+ const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
1216
+ let releaseNext;
1217
+ const next = new Promise((resolve2) => {
1218
+ releaseNext = resolve2;
1219
+ });
1220
+ this.#nativeGrantQueues.set(ownerDid, next);
1221
+ await previous;
1222
+ try {
1223
+ return await action();
1224
+ } finally {
1225
+ releaseNext();
1226
+ if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
1227
+ }
1228
+ }
1229
+ async #loadNativeServiceKey() {
1230
+ if (this.#nativeIdentity === void 0) {
1231
+ const loading = this.#readOrCreateNativeServiceKey();
1232
+ this.#nativeIdentity = loading;
1233
+ void loading.catch(() => {
1234
+ if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
1235
+ });
1236
+ }
1237
+ return this.#nativeIdentity;
1238
+ }
1239
+ async #readOrCreateNativeServiceKey() {
1240
+ const path = join(this.#config.stateDir, ".tinycloud", "native-service-key.json");
1241
+ await secureDirectory(dirname(path));
1242
+ let stored = await readJson(path);
1243
+ if (stored === null) {
1244
+ const generated = generateSessionKey();
1245
+ try {
1246
+ await writeFile(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
1247
+ `, {
1248
+ mode: 384,
1249
+ flag: "wx"
1250
+ });
1251
+ } catch (error) {
1252
+ if (!isRecord(error) || error.code !== "EEXIST") throw error;
1253
+ }
1254
+ await chmod(path, 384);
1255
+ stored = await readJson(path);
1256
+ }
1257
+ if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
1258
+ throw new Error("The native MCP service identity is unavailable.");
1259
+ }
1260
+ return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
1261
+ }
1262
+ async #activateNativeDelegation(serialized, identity) {
1263
+ let delegation;
1264
+ try {
1265
+ const parsed = JSON.parse(serialized);
1266
+ if (!isRecord(parsed)) throw new Error("invalid delegation");
1267
+ delegation = deserializeDelegation(serialized);
1268
+ } catch {
1269
+ throw new NativeGrantValidationError("native_grant_rejected");
1270
+ }
1271
+ 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())) {
1272
+ throw new NativeGrantValidationError("native_grant_rejected");
1273
+ }
1274
+ const now = Date.now();
1275
+ const claimedExpiry = delegation.expiry.getTime();
1276
+ if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
1277
+ if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1278
+ throw new NativeGrantValidationError("native_grant_rejected");
1279
+ }
1280
+ let ownerDid;
1281
+ let spaceId;
1282
+ const node = new TinyCloudNode({ host: this.#config.nodeHost });
1283
+ try {
1284
+ await node.restoreSession({
1285
+ delegationHeader: delegation.delegationHeader,
1286
+ delegationCid: delegation.cid,
1287
+ spaceId: delegation.spaceId,
1288
+ jwk: identity.jwk,
1289
+ verificationMethod: identity.did,
1290
+ tinycloudHosts: [this.#config.nodeHost]
1291
+ });
1292
+ const activated = await activateValidatedRuntimeDelegation(node, delegation, {
1293
+ host: this.#config.nodeHost,
1294
+ authorize: (permissions) => {
1295
+ const derivedOwner = nativeOwnerForConnectorGrant(permissions);
1296
+ const signedSpace = permissions[0]?.space;
1297
+ if (derivedOwner === void 0 || signedSpace === void 0) return false;
1298
+ const metadataOwner = canonicalOwnerDid(
1299
+ `did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
1300
+ );
1301
+ if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
1302
+ ownerDid = derivedOwner;
1303
+ spaceId = signedSpace;
1304
+ return true;
1305
+ }
1306
+ });
1307
+ if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1308
+ throw new NativeGrantValidationError("native_grant_rejected");
1309
+ }
1310
+ return {
1311
+ ownerDid,
1312
+ spaceId,
1313
+ permissions: [...activated.effectivePermissions],
1314
+ cid: activated.cid,
1315
+ expiresAt: activated.expiry,
1316
+ delegation: activated.delegation
1317
+ };
1318
+ } catch (error) {
1319
+ if (error instanceof NativeGrantValidationError) throw error;
1320
+ if (error instanceof RuntimeDelegationRejectedError) {
1321
+ throw new NativeGrantValidationError(
1322
+ error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
1323
+ );
1324
+ }
1325
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
1326
+ }
1327
+ }
1328
+ #nativeGrantPath(stateRoot) {
1329
+ return join(stateRoot, ".tinycloud", "native-grant.json");
1330
+ }
956
1331
  #tenantId(subject) {
957
1332
  return createHmac("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
958
1333
  }
@@ -1046,6 +1421,21 @@ function samePrincipal(left, right) {
1046
1421
  function normalizeDid(value) {
1047
1422
  return value.split("#", 1)[0];
1048
1423
  }
1424
+ function sameNodeHost(left, right) {
1425
+ try {
1426
+ const normalize = (value) => {
1427
+ const url = new URL(value);
1428
+ if (url.username !== "" || url.password !== "") return "";
1429
+ url.hash = "";
1430
+ url.search = "";
1431
+ url.pathname = url.pathname.replace(/\/+$/, "");
1432
+ return url.toString().replace(/\/+$/, "");
1433
+ };
1434
+ return normalize(left) !== "" && normalize(left) === normalize(right);
1435
+ } catch {
1436
+ return false;
1437
+ }
1438
+ }
1049
1439
  function isPermissionEntry(value) {
1050
1440
  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");
1051
1441
  }
@@ -1076,6 +1466,23 @@ async function readJson(path) {
1076
1466
  throw error;
1077
1467
  }
1078
1468
  }
1469
+ async function readOptionalText(path) {
1470
+ try {
1471
+ return await readFile(path, "utf8");
1472
+ } catch (error) {
1473
+ if (isRecord(error) && error.code === "ENOENT") return null;
1474
+ throw error;
1475
+ }
1476
+ }
1477
+ async function restoreOptionalText(path, contents) {
1478
+ if (contents === null) {
1479
+ await rm(path, { force: true });
1480
+ return;
1481
+ }
1482
+ await secureDirectory(dirname(path));
1483
+ await writeFile(path, contents, { mode: 384 });
1484
+ await chmod(path, 384);
1485
+ }
1079
1486
  async function reusablePending(directory, expected) {
1080
1487
  let names;
1081
1488
  try {
@@ -1472,7 +1879,7 @@ function createJsonSchemaValidator() {
1472
1879
  return new AjvJsonSchemaValidator();
1473
1880
  }
1474
1881
  function registerTinyCloudTools(server, selection, validator) {
1475
- const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
1882
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
1476
1883
  for (const binding of bindings) {
1477
1884
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
1478
1885
  if (operation === void 0) {
@@ -1550,7 +1957,7 @@ function outcomeCode(result) {
1550
1957
  // package.json
1551
1958
  var package_default = {
1552
1959
  name: "@tinycloud/mcp",
1553
- version: "0.8.3-beta.9",
1960
+ version: "0.8.4-beta.0",
1554
1961
  description: "TinyCloud delegated operations MCP server",
1555
1962
  keywords: [
1556
1963
  "tinycloud",
@@ -1621,9 +2028,9 @@ var package_default = {
1621
2028
  dependencies: {
1622
2029
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1623
2030
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1624
- "@tinycloud/node-sdk": "3.8.0-beta.7",
2031
+ "@tinycloud/node-sdk": "3.8.1-beta.0",
1625
2032
  "@tinycloud/node-sdk-wasm": "1.7.8",
1626
- "@tinycloud/operations": "0.7.7-beta.8",
2033
+ "@tinycloud/operations": "0.7.8-beta.0",
1627
2034
  jose: "6.2.4"
1628
2035
  },
1629
2036
  devDependencies: {
@@ -1647,13 +2054,19 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
1647
2054
 
1648
2055
  // src/http.ts
1649
2056
  var REQUIRED_SCOPE = "tinycloud:mcp";
2057
+ var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
2058
+ var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
1650
2059
  function createHostedMcpApp(config) {
2060
+ if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
2061
+ throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
2062
+ }
1651
2063
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1652
2064
  const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1653
2065
  const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
1654
2066
  const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
1655
2067
  const plugin = createHostedSurface({
1656
2068
  ...config,
2069
+ nativeServiceCredential: void 0,
1657
2070
  stateDir: join3(config.stateDir, "chatgpt"),
1658
2071
  approvalPath: new URL("./connect", pluginUrl).pathname
1659
2072
  }, pluginUrl, "public", statusFor(pluginUrl));
@@ -1689,16 +2102,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1689
2102
  resourceMetadataUrl
1690
2103
  });
1691
2104
  const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
2105
+ const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
2106
+ if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
1692
2107
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1693
- const identity = oauthIdentity(authInfo2);
2108
+ const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
2109
+ const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
1694
2110
  const { subject, ownerDids } = identity;
1695
- const stateRoot = await store.initializeTenant(subject);
2111
+ const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
1696
2112
  const validator = createJsonSchemaValidator();
1697
2113
  const server = new McpServer2(
1698
2114
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1699
2115
  {
1700
2116
  jsonSchemaValidator: validator,
1701
- 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."
2117
+ 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."
1702
2118
  }
1703
2119
  );
1704
2120
  registerTinyCloudTools(server, {
@@ -1707,10 +2123,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1707
2123
  explicitProfile: true,
1708
2124
  allowOwnerProfile: false,
1709
2125
  stateRoot,
1710
- beforeOperation: () => store.validateTenant(subject, ownerDids),
1711
- transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2126
+ ...nativeOwner === void 0 ? {} : {
2127
+ allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
2128
+ },
2129
+ beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
2130
+ ...nativeOwner === void 0 ? {
2131
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2132
+ } : {}
1712
2133
  }, validator);
1713
- registerConnectTool(server, validator, store, identity, surface);
2134
+ if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
1714
2135
  return server;
1715
2136
  }, {
1716
2137
  legacy: "stateless",
@@ -1726,14 +2147,205 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1726
2147
  const allowedHosts = [resourceUrl.hostname];
1727
2148
  return {
1728
2149
  async fetch(request) {
1729
- const metadata = oauthMetadataResponse(request, metadataOptions);
1730
- if (metadata !== void 0) return metadata;
1731
2150
  const url = new URL(request.url);
2151
+ if (url.pathname !== "/healthz") {
2152
+ const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
2153
+ if (hostFailure !== void 0) return hostFailure;
2154
+ }
2155
+ if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
2156
+ const metadataUrl = new URL(getOAuthProtectedResourceMetadataUrl(resourceUrl));
2157
+ const metadataRequest = new Request(metadataUrl.toString(), {
2158
+ method: request.method,
2159
+ headers: request.headers
2160
+ });
2161
+ const response = oauthMetadataResponse(metadataRequest, metadataOptions);
2162
+ if (response !== void 0) return withBearerMethodsSupported(response);
2163
+ }
2164
+ const metadata = oauthMetadataResponse(request, metadataOptions);
2165
+ if (metadata !== void 0) {
2166
+ return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
2167
+ }
2168
+ if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
2169
+ if (request.method === "OPTIONS") {
2170
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2171
+ }
2172
+ if (request.method !== "GET") return methodNotAllowed("GET");
2173
+ return publicMetadataCors(Response.json({
2174
+ specVersion: "1.0",
2175
+ host: {
2176
+ displayName: "TinyCloud, Inc.",
2177
+ identifier: "https://mcp.tinycloud.xyz",
2178
+ documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
2179
+ },
2180
+ entries: [{
2181
+ identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
2182
+ displayName: "TinyCloud MCP",
2183
+ type: "application/json",
2184
+ description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
2185
+ url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
2186
+ representativeQueries: [
2187
+ "How do I connect to TinyCloud's hosted MCP server?",
2188
+ "Which endpoint provides TinyCloud's delegated MCP operations?",
2189
+ "How can I connect to TinyCloud MCP using local stdio?"
2190
+ ]
2191
+ }]
2192
+ }, {
2193
+ headers: {
2194
+ "cache-control": "public, max-age=300",
2195
+ "content-type": "application/json; charset=utf-8"
2196
+ }
2197
+ }), request);
2198
+ }
2199
+ if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
2200
+ if (request.method === "OPTIONS") {
2201
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2202
+ }
2203
+ if (request.method !== "GET") return methodNotAllowed("GET");
2204
+ return publicMetadataCors(Response.json({
2205
+ protocol_version: "0.1",
2206
+ organization: {
2207
+ name: "TinyCloud, Inc.",
2208
+ domain: resourceUrl.hostname
2209
+ },
2210
+ web: {}
2211
+ }, {
2212
+ headers: {
2213
+ "cache-control": "public, max-age=300",
2214
+ "content-type": "application/json; charset=utf-8"
2215
+ }
2216
+ }), request);
2217
+ }
2218
+ if (url.pathname === "/auth.md" && surface === "hosted") {
2219
+ if (request.method === "OPTIONS") {
2220
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2221
+ }
2222
+ if (request.method !== "GET") return methodNotAllowed("GET");
2223
+ const issuerUrl = new URL(issuer);
2224
+ const authorizationServerMetadataUrl = new URL(
2225
+ `/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
2226
+ issuerUrl.origin
2227
+ );
2228
+ const registrationEndpoint = config.oauthMetadata.registration_endpoint;
2229
+ 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.";
2230
+ const authDoc = [
2231
+ "# TinyCloud MCP auth.md",
2232
+ "",
2233
+ `Protected resource: ${resourceUrl}.`,
2234
+ "",
2235
+ "Send an OAuth access token in the `Authorization: Bearer` header.",
2236
+ "",
2237
+ `Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
2238
+ `Authorization server issuer: ${issuer}.`,
2239
+ `Authorization server metadata: ${authorizationServerMetadataUrl}.`,
2240
+ `Scope: \`${REQUIRED_SCOPE}\`.`,
2241
+ "",
2242
+ "Hosted access uses OpenKey OAuth.",
2243
+ registrationNote,
2244
+ "TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
2245
+ "No `agent_auth` registration block is published because no agent-registration endpoint is available.",
2246
+ "",
2247
+ `Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
2248
+ "",
2249
+ "Connection steps: https://docs.tinycloud.xyz/cli/mcp"
2250
+ ].join("\n") + "\n";
2251
+ return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
2252
+ }
2253
+ if (url.pathname === "/robots.txt" && surface === "hosted") {
2254
+ return request.method === "GET" ? publicText(`User-agent: *
2255
+ Allow: /
2256
+ Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
2257
+ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
2258
+ `, "text/plain; charset=utf-8") : methodNotAllowed("GET");
2259
+ }
2260
+ if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
2261
+ if (request.method === "OPTIONS") {
2262
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2263
+ }
2264
+ if (request.method !== "GET") return methodNotAllowed("GET");
2265
+ const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
2266
+ const card = {
2267
+ $schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
2268
+ name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
2269
+ title: "TinyCloud MCP",
2270
+ version: MCP_VERSION,
2271
+ description: "MCP server for delegated TinyCloud operations.",
2272
+ websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
2273
+ repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
2274
+ remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
2275
+ };
2276
+ if (identity !== void 0) {
2277
+ card["x-tinycloud-native-auth"] = {
2278
+ serviceDid: identity.serviceDid,
2279
+ serviceDidField: "x-tinycloud-native-auth.serviceDid",
2280
+ grantEndpoint: {
2281
+ method: "POST",
2282
+ path: "/native/grant",
2283
+ body: { delegation: "serialized PortableDelegation JSON string" }
2284
+ },
2285
+ serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
2286
+ serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
2287
+ ownerDidHeader: NATIVE_OWNER_DID_HEADER,
2288
+ maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
2289
+ acceptedCapabilities: [
2290
+ { service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
2291
+ { service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
2292
+ ],
2293
+ tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
2294
+ errors: {
2295
+ required: NATIVE_GRANT_REQUIRED,
2296
+ expired: NATIVE_GRANT_EXPIRED,
2297
+ rejected: NATIVE_GRANT_REJECTED,
2298
+ serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
2299
+ serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
2300
+ validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
2301
+ }
2302
+ };
2303
+ }
2304
+ return publicMetadataCors(Response.json(card, {
2305
+ headers: { "cache-control": "public, max-age=300" }
2306
+ }), request);
2307
+ }
2308
+ if (url.pathname === "/llms.txt" && surface === "hosted") {
2309
+ return request.method === "GET" ? publicText(`# TinyCloud MCP
2310
+
2311
+ Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
2312
+
2313
+ Connection documentation: https://docs.tinycloud.xyz/cli/mcp
2314
+
2315
+ Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
2316
+ `, "text/plain; charset=utf-8") : methodNotAllowed("GET");
2317
+ }
2318
+ if (url.pathname === "/sitemap.xml" && surface === "hosted") {
2319
+ 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>`;
2320
+ return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
2321
+ }
2322
+ if (url.pathname === "/" && surface === "hosted") {
2323
+ if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
2324
+ const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
2325
+ const content = markdown ? `# TinyCloud MCP
2326
+
2327
+ Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
2328
+
2329
+ [Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
2330
+
2331
+ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
2332
+ ` : `<!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>`;
2333
+ const headers = new Headers({
2334
+ "content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
2335
+ "cache-control": "public, max-age=300",
2336
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
2337
+ "x-content-type-options": "nosniff",
2338
+ vary: "Accept"
2339
+ });
2340
+ headers.set(
2341
+ "link",
2342
+ `<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
2343
+ );
2344
+ return new Response(request.method === "HEAD" ? null : content, { headers });
2345
+ }
1732
2346
  if (url.pathname === "/healthz") {
1733
2347
  return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
1734
2348
  }
1735
- const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
1736
- if (hostFailure !== void 0) return hostFailure;
1737
2349
  if (url.pathname === connectPath) {
1738
2350
  if (request.method !== "GET") return methodNotAllowed("GET");
1739
2351
  try {
@@ -1789,6 +2401,73 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1789
2401
  );
1790
2402
  }
1791
2403
  }
2404
+ if (url.pathname === "/native/grant" && surface === "hosted") {
2405
+ if (request.method !== "POST") return methodNotAllowed("POST");
2406
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2407
+ if (config.nativeServiceCredential === void 0) {
2408
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2409
+ }
2410
+ if (!serviceCredentialMatches(
2411
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2412
+ config.nativeServiceCredential
2413
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2414
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
2415
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2416
+ }
2417
+ const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
2418
+ if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
2419
+ let body;
2420
+ try {
2421
+ body = JSON.parse(bodyText);
2422
+ } catch {
2423
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2424
+ }
2425
+ if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
2426
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2427
+ }
2428
+ const grantBody = body;
2429
+ if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
2430
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2431
+ }
2432
+ try {
2433
+ const accepted = await store.acceptNativeGrant(grantBody.delegation);
2434
+ return json({
2435
+ ownerDid: accepted.ownerDid,
2436
+ serviceDid: accepted.serviceDid,
2437
+ expiresAt: accepted.expiresAt
2438
+ }, 201);
2439
+ } catch (error) {
2440
+ return nativeGrantErrorResponse(error);
2441
+ }
2442
+ }
2443
+ if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
2444
+ if (config.nativeServiceCredential === void 0) {
2445
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2446
+ }
2447
+ if (!serviceCredentialMatches(
2448
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2449
+ config.nativeServiceCredential
2450
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2451
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2452
+ const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
2453
+ if (ownerHeader === null || ownerHeader.length === 0) {
2454
+ return json({ error: NATIVE_GRANT_REQUIRED }, 401);
2455
+ }
2456
+ const grant = await store.nativeGrantForOwner(ownerHeader);
2457
+ if (grant.status !== "ok") {
2458
+ const status = grant.status === "unavailable" ? 503 : 401;
2459
+ const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
2460
+ return json({ error: code }, status);
2461
+ }
2462
+ return handler.fetch(request, {
2463
+ authInfo: {
2464
+ token: "native-service",
2465
+ clientId: "tinycloud-native",
2466
+ scopes: [],
2467
+ extra: { nativeOwnerDid: grant.ownerDid }
2468
+ }
2469
+ });
2470
+ }
1792
2471
  if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
1793
2472
  const originFailure = validateOrigin(request, config.allowedOrigins);
1794
2473
  if (originFailure !== void 0) return originFailure;
@@ -1900,9 +2579,77 @@ function callbackCors(response, openkeyHost) {
1900
2579
  headers.set("vary", "Origin");
1901
2580
  return new Response(response.body, { status: response.status, headers });
1902
2581
  }
2582
+ async function readBoundedBodyText(request, maxBytes) {
2583
+ const contentLength = request.headers.get("content-length");
2584
+ if (contentLength !== null) {
2585
+ const parsedLength = Number(contentLength);
2586
+ if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
2587
+ }
2588
+ if (request.body === null) return "";
2589
+ const reader = request.body.getReader();
2590
+ const chunks = [];
2591
+ let total = 0;
2592
+ try {
2593
+ while (true) {
2594
+ const { done, value } = await reader.read();
2595
+ if (done) break;
2596
+ total += value.byteLength;
2597
+ if (total > maxBytes) {
2598
+ await reader.cancel();
2599
+ return void 0;
2600
+ }
2601
+ chunks.push(value);
2602
+ }
2603
+ } finally {
2604
+ reader.releaseLock();
2605
+ }
2606
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
2607
+ }
2608
+ function nativeGrantErrorResponse(error) {
2609
+ if (!(error instanceof NativeGrantValidationError)) {
2610
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2611
+ }
2612
+ const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
2613
+ return json({ error: error.code }, unavailable ? 503 : 401);
2614
+ }
1903
2615
  function json(value, status = 200) {
1904
2616
  return Response.json(value, { status, headers: { "cache-control": "no-store" } });
1905
2617
  }
2618
+ async function withBearerMethodsSupported(response) {
2619
+ if (response.status === 204 || response.body === null) return response;
2620
+ const metadata = await response.json();
2621
+ const headers = new Headers(response.headers);
2622
+ headers.delete("content-length");
2623
+ headers.delete("content-encoding");
2624
+ return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
2625
+ status: response.status,
2626
+ statusText: response.statusText,
2627
+ headers
2628
+ });
2629
+ }
2630
+ function publicMetadataCors(response, request) {
2631
+ const headers = new Headers(response.headers);
2632
+ headers.set("access-control-allow-origin", "*");
2633
+ headers.set("access-control-allow-methods", "GET, OPTIONS");
2634
+ const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
2635
+ if (requestedHeaders !== null) {
2636
+ headers.set("access-control-allow-headers", requestedHeaders);
2637
+ }
2638
+ headers.set(
2639
+ "vary",
2640
+ requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
2641
+ );
2642
+ return new Response(response.body, { status: response.status, headers });
2643
+ }
2644
+ function publicText(text, contentType) {
2645
+ return new Response(text, {
2646
+ headers: {
2647
+ "content-type": contentType,
2648
+ "cache-control": "public, max-age=300",
2649
+ "x-content-type-options": "nosniff"
2650
+ }
2651
+ });
2652
+ }
1906
2653
  function methodNotAllowed(allow) {
1907
2654
  return new Response("Method not allowed", { status: 405, headers: { allow } });
1908
2655
  }
@@ -1932,6 +2679,7 @@ async function main() {
1932
2679
  approvalTtlSeconds: positiveInteger(process.env.TC_MCP_APPROVAL_TTL_SECONDS, 300),
1933
2680
  delegationExpiry: process.env.TC_MCP_DELEGATION_EXPIRY ?? "1h",
1934
2681
  allowedOrigins: commaList(process.env.TC_MCP_ALLOWED_ORIGINS),
2682
+ ...process.env.TC_MCP_NATIVE_SERVICE_CREDENTIAL === void 0 ? {} : { nativeServiceCredential: process.env.TC_MCP_NATIVE_SERVICE_CREDENTIAL },
1935
2683
  oauthMetadata,
1936
2684
  introspection: {
1937
2685
  endpoint: requiredUrl("TC_MCP_INTROSPECTION_URL").toString(),