@tinycloud/mcp 0.8.3-beta.16 → 0.8.3-beta.18

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.js CHANGED
@@ -259,12 +259,15 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
259
259
  }
260
260
 
261
261
  // src/remote-store.ts
262
- import { createHmac, randomBytes, timingSafeEqual } from "crypto";
262
+ import { createHmac, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
263
263
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
264
264
  import { dirname, join } from "path";
265
265
  import {
266
+ activateValidatedRuntimeDelegation,
267
+ deserializeDelegation,
266
268
  InvalidRestoredSessionError,
267
269
  RuntimeDelegationRejectedError,
270
+ serializeDelegation,
268
271
  TinyCloudNode,
269
272
  UnsupportedSessionRestoreError,
270
273
  isCapabilitySubset,
@@ -273,11 +276,80 @@ import {
273
276
  signedCompactUcanExpiry
274
277
  } from "@tinycloud/node-sdk";
275
278
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
279
+ import { createOrReusePermissionRequest } from "@tinycloud/operations/artifacts";
280
+ import { withTinyCloudStateRoot } from "@tinycloud/operations/state";
276
281
  import { invokeOperation } from "@tinycloud/operations";
277
282
  import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
278
283
  import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
284
+
285
+ // src/native-auth.ts
286
+ import { timingSafeEqual } from "crypto";
287
+ var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
288
+ var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
289
+ var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
290
+ var NATIVE_GRANT_REQUIRED = "native_grant_required";
291
+ var NATIVE_GRANT_EXPIRED = "native_grant_expired";
292
+ var NATIVE_GRANT_REJECTED = "native_grant_rejected";
293
+ var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
294
+ var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
295
+ var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
296
+ var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
297
+ function serviceCredentialMatches(supplied, configured) {
298
+ if (supplied === null || configured === void 0) return false;
299
+ const suppliedBytes = Buffer.from(supplied, "utf8");
300
+ const configuredBytes = Buffer.from(configured, "utf8");
301
+ return suppliedBytes.byteLength === configuredBytes.byteLength && timingSafeEqual(suppliedBytes, configuredBytes);
302
+ }
303
+ var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
304
+ var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
305
+ var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
306
+ function canonicalOwnerDid(value) {
307
+ const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
308
+ if (!match) return void 0;
309
+ const chainId = Number(match[1]);
310
+ if (!Number.isSafeInteger(chainId)) return void 0;
311
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
312
+ }
313
+ function nativeOwnerForConnectorGrant(permissions) {
314
+ if (permissions.length !== 2) return void 0;
315
+ const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
316
+ const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
317
+ if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
318
+ const sql = sqlEntries[0];
319
+ const kv = kvEntries[0];
320
+ if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
321
+ if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
322
+ if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
323
+ const sqlSpace = ownerFromApplicationsSpace(sql.space);
324
+ const kvSpace = ownerFromApplicationsSpace(kv.space);
325
+ return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
326
+ }
327
+ function ownerFromApplicationsSpace(space) {
328
+ const match = OWNER_APPLICATIONS_SPACE.exec(space);
329
+ if (!match) return void 0;
330
+ const chainId = Number(match[1]);
331
+ if (!Number.isSafeInteger(chainId)) return void 0;
332
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
333
+ }
334
+ function actionsEqual(actual, expected) {
335
+ const actualActions = [...actual].sort();
336
+ const expectedActions = [...expected].sort();
337
+ return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
338
+ }
339
+ function hasCaveats(permission) {
340
+ return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
341
+ }
342
+
343
+ // src/remote-store.ts
279
344
  var PROFILE = "agent";
280
345
  var MAX_CALLBACK_BYTES = 256 * 1024;
346
+ var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
347
+ "DELEGATION_ARTIFACT_INVALID",
348
+ "DELEGATION_EXPIRED",
349
+ "DELEGATION_AUDIENCE_MISMATCH",
350
+ "DELEGATION_HOST_MISMATCH",
351
+ "DELEGATION_REJECTED"
352
+ ]);
281
353
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
282
354
  var BOOTSTRAP_PERMISSIONS = [
283
355
  {
@@ -319,6 +391,15 @@ var ApprovalRequestError = class extends Error {
319
391
  this.detail = detail;
320
392
  }
321
393
  };
394
+ var NativeGrantValidationError = class extends Error {
395
+ code;
396
+ constructor(status) {
397
+ 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";
398
+ super(code);
399
+ this.name = "NativeGrantValidationError";
400
+ this.code = code;
401
+ }
402
+ };
322
403
  var RemoteTenantStore = class {
323
404
  #config;
324
405
  #initializing = /* @__PURE__ */ new Map();
@@ -331,9 +412,186 @@ var RemoteTenantStore = class {
331
412
  #scopedClearing = /* @__PURE__ */ new Map();
332
413
  #clearedScoped = /* @__PURE__ */ new Map();
333
414
  #approvalProcessing = /* @__PURE__ */ new Map();
415
+ #nativeIdentity;
416
+ #nativeGrantQueues = /* @__PURE__ */ new Map();
334
417
  constructor(config) {
335
418
  this.#config = config;
336
419
  }
420
+ async nativeServiceIdentity() {
421
+ const identity = await this.#loadNativeServiceKey();
422
+ return { serviceDid: normalizeDid(identity.did) };
423
+ }
424
+ nativeTenantStateRoot(ownerDid) {
425
+ const canonical = canonicalOwnerDid(ownerDid);
426
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
427
+ const tenant = createHmac("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
428
+ return join(this.#config.stateDir, "native-tenants", tenant);
429
+ }
430
+ async acceptNativeGrant(serializedDelegation) {
431
+ if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
432
+ throw new NativeGrantValidationError("rejected");
433
+ }
434
+ const identity = await this.#loadNativeServiceKey();
435
+ const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
436
+ const { ownerDid, spaceId, permissions, expiresAt } = activated;
437
+ return this.#withNativeGrantLock(ownerDid, async () => {
438
+ const stateRoot = this.nativeTenantStateRoot(ownerDid);
439
+ let previous = null;
440
+ try {
441
+ const stored = await readJson(this.#nativeGrantPath(stateRoot));
442
+ 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))) {
443
+ previous = stored;
444
+ }
445
+ } catch (error) {
446
+ if (!(error instanceof SyntaxError)) throw error;
447
+ }
448
+ if (previous !== null) {
449
+ const previousExpiry = Date.parse(previous.expiresAt);
450
+ const nextExpiry = expiresAt.getTime();
451
+ if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
452
+ throw new NativeGrantValidationError("rejected");
453
+ }
454
+ if (previous.delegationCid === activated.cid) {
455
+ return {
456
+ ownerDid,
457
+ serviceDid: normalizeDid(identity.did),
458
+ expiresAt: previous.expiresAt,
459
+ stateRoot
460
+ };
461
+ }
462
+ }
463
+ const session = {
464
+ delegationHeader: activated.delegation.delegationHeader,
465
+ delegationCid: activated.cid,
466
+ spaceId,
467
+ jwk: identity.jwk,
468
+ verificationMethod: identity.did,
469
+ tinycloudHosts: [this.#config.nodeHost]
470
+ };
471
+ const grant = {
472
+ version: 1,
473
+ ownerDid,
474
+ serviceDid: normalizeDid(identity.did),
475
+ serializedDelegation: serializeDelegation(activated.delegation),
476
+ delegationCid: activated.cid,
477
+ spaceId,
478
+ permissions,
479
+ expiresAt: expiresAt.toISOString()
480
+ };
481
+ const sessionPath = this.#sessionPath(stateRoot);
482
+ const previousSession = await readOptionalText(sessionPath);
483
+ try {
484
+ await secureWrite(this.#profilePath(stateRoot), {
485
+ name: PROFILE,
486
+ host: this.#config.nodeHost,
487
+ chainId: activated.delegation.chainId,
488
+ spaceName: "applications",
489
+ did: identity.did,
490
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
491
+ profile: PROFILE,
492
+ operatorType: "agent",
493
+ posture: "delegate-session",
494
+ ownerDid,
495
+ sessionDid: identity.did,
496
+ spaceId,
497
+ tinycloudHost: this.#config.nodeHost
498
+ });
499
+ await secureWrite(this.#keyPath(stateRoot), identity.jwk);
500
+ await secureWrite(sessionPath, session);
501
+ const request = await withTinyCloudStateRoot(stateRoot, () => createOrReusePermissionRequest({
502
+ profile: PROFILE,
503
+ posture: "delegate-session",
504
+ operatorType: "agent",
505
+ host: this.#config.nodeHost,
506
+ sessionDid: identity.did,
507
+ ownerDid,
508
+ spaceId,
509
+ missing: permissions,
510
+ granted: [],
511
+ replace: true
512
+ }));
513
+ if (request.status !== "created") {
514
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
515
+ }
516
+ const imported = await invokeOperation(
517
+ "tinycloud.auth.import",
518
+ 1,
519
+ { profile: PROFILE, stateRoot },
520
+ {
521
+ kind: "tinycloud.auth.delegation",
522
+ version: 1,
523
+ requestId: request.request.requestId,
524
+ delegationCid: activated.cid,
525
+ delegation: activated.delegation
526
+ }
527
+ );
528
+ if (imported.status === "error") {
529
+ throw new NativeGrantValidationError(
530
+ NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
531
+ );
532
+ }
533
+ if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
534
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
535
+ }
536
+ await secureWrite(this.#nativeGrantPath(stateRoot), grant);
537
+ } catch (error) {
538
+ try {
539
+ await restoreOptionalText(sessionPath, previousSession);
540
+ } catch {
541
+ await rm(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
542
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
543
+ }
544
+ throw error;
545
+ }
546
+ return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
547
+ });
548
+ }
549
+ async nativeGrantForOwner(ownerDid) {
550
+ const canonical = canonicalOwnerDid(ownerDid);
551
+ if (canonical === void 0) return { status: "rejected" };
552
+ const stateRoot = this.nativeTenantStateRoot(canonical);
553
+ let stored;
554
+ try {
555
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
556
+ } catch {
557
+ return { status: "rejected" };
558
+ }
559
+ if (stored === null) return { status: "required" };
560
+ 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) {
561
+ return { status: "rejected" };
562
+ }
563
+ const expires = Date.parse(stored.expiresAt);
564
+ if (!Number.isFinite(expires)) return { status: "rejected" };
565
+ if (expires <= Date.now()) return { status: "expired" };
566
+ try {
567
+ const identity = await this.#loadNativeServiceKey();
568
+ if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
569
+ const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
570
+ 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" };
571
+ return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
572
+ } catch (error) {
573
+ if (error instanceof NativeGrantValidationError) {
574
+ return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
575
+ }
576
+ return { status: "unavailable" };
577
+ }
578
+ }
579
+ async assertNativeGrantActive(ownerDid) {
580
+ const canonical = canonicalOwnerDid(ownerDid);
581
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
582
+ const stateRoot = this.nativeTenantStateRoot(canonical);
583
+ let stored;
584
+ try {
585
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
586
+ } catch {
587
+ throw new NativeGrantValidationError("rejected");
588
+ }
589
+ if (stored === null) throw new NativeGrantValidationError("required");
590
+ if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
591
+ throw new NativeGrantValidationError("rejected");
592
+ }
593
+ if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
594
+ }
337
595
  tenantStateRoot(subject) {
338
596
  return join(this.#config.stateDir, "tenants", this.#tenantId(subject));
339
597
  }
@@ -885,7 +1143,7 @@ var RemoteTenantStore = class {
885
1143
  const expected = this.#signState(tenantId, nonce);
886
1144
  const supplied = Buffer.from(signature, "base64url");
887
1145
  const expectedBytes = Buffer.from(expected, "base64url");
888
- if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
1146
+ if (supplied.length !== expectedBytes.length || !timingSafeEqual2(supplied, expectedBytes)) {
889
1147
  throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
890
1148
  }
891
1149
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
@@ -936,6 +1194,123 @@ var RemoteTenantStore = class {
936
1194
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
937
1195
  return url.toString();
938
1196
  }
1197
+ async #withNativeGrantLock(ownerDid, action) {
1198
+ const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
1199
+ let releaseNext;
1200
+ const next = new Promise((resolve) => {
1201
+ releaseNext = resolve;
1202
+ });
1203
+ this.#nativeGrantQueues.set(ownerDid, next);
1204
+ await previous;
1205
+ try {
1206
+ return await action();
1207
+ } finally {
1208
+ releaseNext();
1209
+ if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
1210
+ }
1211
+ }
1212
+ async #loadNativeServiceKey() {
1213
+ if (this.#nativeIdentity === void 0) {
1214
+ const loading = this.#readOrCreateNativeServiceKey();
1215
+ this.#nativeIdentity = loading;
1216
+ void loading.catch(() => {
1217
+ if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
1218
+ });
1219
+ }
1220
+ return this.#nativeIdentity;
1221
+ }
1222
+ async #readOrCreateNativeServiceKey() {
1223
+ const path = join(this.#config.stateDir, ".tinycloud", "native-service-key.json");
1224
+ await secureDirectory(dirname(path));
1225
+ let stored = await readJson(path);
1226
+ if (stored === null) {
1227
+ const generated = generateSessionKey();
1228
+ try {
1229
+ await writeFile(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
1230
+ `, {
1231
+ mode: 384,
1232
+ flag: "wx"
1233
+ });
1234
+ } catch (error) {
1235
+ if (!isRecord(error) || error.code !== "EEXIST") throw error;
1236
+ }
1237
+ await chmod(path, 384);
1238
+ stored = await readJson(path);
1239
+ }
1240
+ if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
1241
+ throw new Error("The native MCP service identity is unavailable.");
1242
+ }
1243
+ return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
1244
+ }
1245
+ async #activateNativeDelegation(serialized, identity) {
1246
+ let delegation;
1247
+ try {
1248
+ const parsed = JSON.parse(serialized);
1249
+ if (!isRecord(parsed)) throw new Error("invalid delegation");
1250
+ delegation = deserializeDelegation(serialized);
1251
+ } catch {
1252
+ throw new NativeGrantValidationError("native_grant_rejected");
1253
+ }
1254
+ 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())) {
1255
+ throw new NativeGrantValidationError("native_grant_rejected");
1256
+ }
1257
+ const now = Date.now();
1258
+ const claimedExpiry = delegation.expiry.getTime();
1259
+ if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
1260
+ if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1261
+ throw new NativeGrantValidationError("native_grant_rejected");
1262
+ }
1263
+ let ownerDid;
1264
+ let spaceId;
1265
+ const node = new TinyCloudNode({ host: this.#config.nodeHost });
1266
+ try {
1267
+ await node.restoreSession({
1268
+ delegationHeader: delegation.delegationHeader,
1269
+ delegationCid: delegation.cid,
1270
+ spaceId: delegation.spaceId,
1271
+ jwk: identity.jwk,
1272
+ verificationMethod: identity.did,
1273
+ tinycloudHosts: [this.#config.nodeHost]
1274
+ });
1275
+ const activated = await activateValidatedRuntimeDelegation(node, delegation, {
1276
+ host: this.#config.nodeHost,
1277
+ authorize: (permissions) => {
1278
+ const derivedOwner = nativeOwnerForConnectorGrant(permissions);
1279
+ const signedSpace = permissions[0]?.space;
1280
+ if (derivedOwner === void 0 || signedSpace === void 0) return false;
1281
+ const metadataOwner = canonicalOwnerDid(
1282
+ `did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
1283
+ );
1284
+ if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
1285
+ ownerDid = derivedOwner;
1286
+ spaceId = signedSpace;
1287
+ return true;
1288
+ }
1289
+ });
1290
+ if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1291
+ throw new NativeGrantValidationError("native_grant_rejected");
1292
+ }
1293
+ return {
1294
+ ownerDid,
1295
+ spaceId,
1296
+ permissions: [...activated.effectivePermissions],
1297
+ cid: activated.cid,
1298
+ expiresAt: activated.expiry,
1299
+ delegation: activated.delegation
1300
+ };
1301
+ } catch (error) {
1302
+ if (error instanceof NativeGrantValidationError) throw error;
1303
+ if (error instanceof RuntimeDelegationRejectedError) {
1304
+ throw new NativeGrantValidationError(
1305
+ error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
1306
+ );
1307
+ }
1308
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
1309
+ }
1310
+ }
1311
+ #nativeGrantPath(stateRoot) {
1312
+ return join(stateRoot, ".tinycloud", "native-grant.json");
1313
+ }
939
1314
  #tenantId(subject) {
940
1315
  return createHmac("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
941
1316
  }
@@ -1029,6 +1404,21 @@ function samePrincipal(left, right) {
1029
1404
  function normalizeDid(value) {
1030
1405
  return value.split("#", 1)[0];
1031
1406
  }
1407
+ function sameNodeHost(left, right) {
1408
+ try {
1409
+ const normalize = (value) => {
1410
+ const url = new URL(value);
1411
+ if (url.username !== "" || url.password !== "") return "";
1412
+ url.hash = "";
1413
+ url.search = "";
1414
+ url.pathname = url.pathname.replace(/\/+$/, "");
1415
+ return url.toString().replace(/\/+$/, "");
1416
+ };
1417
+ return normalize(left) !== "" && normalize(left) === normalize(right);
1418
+ } catch {
1419
+ return false;
1420
+ }
1421
+ }
1032
1422
  function isPermissionEntry(value) {
1033
1423
  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");
1034
1424
  }
@@ -1059,6 +1449,23 @@ async function readJson(path) {
1059
1449
  throw error;
1060
1450
  }
1061
1451
  }
1452
+ async function readOptionalText(path) {
1453
+ try {
1454
+ return await readFile(path, "utf8");
1455
+ } catch (error) {
1456
+ if (isRecord(error) && error.code === "ENOENT") return null;
1457
+ throw error;
1458
+ }
1459
+ }
1460
+ async function restoreOptionalText(path, contents) {
1461
+ if (contents === null) {
1462
+ await rm(path, { force: true });
1463
+ return;
1464
+ }
1465
+ await secureDirectory(dirname(path));
1466
+ await writeFile(path, contents, { mode: 384 });
1467
+ await chmod(path, 384);
1468
+ }
1062
1469
  async function reusablePending(directory, expected) {
1063
1470
  let names;
1064
1471
  try {
@@ -1455,7 +1862,7 @@ function createJsonSchemaValidator() {
1455
1862
  return new AjvJsonSchemaValidator();
1456
1863
  }
1457
1864
  function registerTinyCloudTools(server, selection, validator) {
1458
- const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
1865
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
1459
1866
  for (const binding of bindings) {
1460
1867
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
1461
1868
  if (operation === void 0) {
@@ -1533,7 +1940,7 @@ function outcomeCode(result) {
1533
1940
  // package.json
1534
1941
  var package_default = {
1535
1942
  name: "@tinycloud/mcp",
1536
- version: "0.8.3-beta.16",
1943
+ version: "0.8.3-beta.18",
1537
1944
  description: "TinyCloud delegated operations MCP server",
1538
1945
  keywords: [
1539
1946
  "tinycloud",
@@ -1604,9 +2011,9 @@ var package_default = {
1604
2011
  dependencies: {
1605
2012
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1606
2013
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1607
- "@tinycloud/node-sdk": "3.8.0-beta.15",
2014
+ "@tinycloud/node-sdk": "3.8.0-beta.16",
1608
2015
  "@tinycloud/node-sdk-wasm": "1.7.8",
1609
- "@tinycloud/operations": "0.7.7-beta.14",
2016
+ "@tinycloud/operations": "0.7.7-beta.15",
1610
2017
  jose: "6.2.4"
1611
2018
  },
1612
2019
  devDependencies: {
@@ -1633,12 +2040,16 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
1633
2040
  var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
1634
2041
  var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
1635
2042
  function createHostedMcpApp(config) {
2043
+ if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
2044
+ throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
2045
+ }
1636
2046
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1637
2047
  const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1638
2048
  const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
1639
2049
  const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
1640
2050
  const plugin = createHostedSurface({
1641
2051
  ...config,
2052
+ nativeServiceCredential: void 0,
1642
2053
  stateDir: join3(config.stateDir, "chatgpt"),
1643
2054
  approvalPath: new URL("./connect", pluginUrl).pathname
1644
2055
  }, pluginUrl, "public", statusFor(pluginUrl));
@@ -1674,16 +2085,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1674
2085
  resourceMetadataUrl
1675
2086
  });
1676
2087
  const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
2088
+ const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
2089
+ if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
1677
2090
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1678
- const identity = oauthIdentity(authInfo2);
2091
+ const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
2092
+ const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
1679
2093
  const { subject, ownerDids } = identity;
1680
- const stateRoot = await store.initializeTenant(subject);
2094
+ const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
1681
2095
  const validator = createJsonSchemaValidator();
1682
2096
  const server = new McpServer2(
1683
2097
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1684
2098
  {
1685
2099
  jsonSchemaValidator: validator,
1686
- 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."
2100
+ 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."
1687
2101
  }
1688
2102
  );
1689
2103
  registerTinyCloudTools(server, {
@@ -1692,10 +2106,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1692
2106
  explicitProfile: true,
1693
2107
  allowOwnerProfile: false,
1694
2108
  stateRoot,
1695
- beforeOperation: () => store.validateTenant(subject, ownerDids),
1696
- transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2109
+ ...nativeOwner === void 0 ? {} : {
2110
+ allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
2111
+ },
2112
+ beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
2113
+ ...nativeOwner === void 0 ? {
2114
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2115
+ } : {}
1697
2116
  }, validator);
1698
- registerConnectTool(server, validator, store, identity, surface);
2117
+ if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
1699
2118
  return server;
1700
2119
  }, {
1701
2120
  legacy: "stateless",
@@ -1826,7 +2245,8 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
1826
2245
  return publicMetadataCors(new Response(null, { status: 204 }), request);
1827
2246
  }
1828
2247
  if (request.method !== "GET") return methodNotAllowed("GET");
1829
- return publicMetadataCors(Response.json({
2248
+ const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
2249
+ const card = {
1830
2250
  $schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
1831
2251
  name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
1832
2252
  title: "TinyCloud MCP",
@@ -1835,7 +2255,38 @@ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
1835
2255
  websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
1836
2256
  repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
1837
2257
  remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
1838
- }, { headers: { "cache-control": "public, max-age=300" } }), request);
2258
+ };
2259
+ if (identity !== void 0) {
2260
+ card["x-tinycloud-native-auth"] = {
2261
+ serviceDid: identity.serviceDid,
2262
+ serviceDidField: "x-tinycloud-native-auth.serviceDid",
2263
+ grantEndpoint: {
2264
+ method: "POST",
2265
+ path: "/native/grant",
2266
+ body: { delegation: "serialized PortableDelegation JSON string" }
2267
+ },
2268
+ serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
2269
+ serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
2270
+ ownerDidHeader: NATIVE_OWNER_DID_HEADER,
2271
+ maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
2272
+ acceptedCapabilities: [
2273
+ { service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
2274
+ { service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
2275
+ ],
2276
+ tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
2277
+ errors: {
2278
+ required: NATIVE_GRANT_REQUIRED,
2279
+ expired: NATIVE_GRANT_EXPIRED,
2280
+ rejected: NATIVE_GRANT_REJECTED,
2281
+ serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
2282
+ serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
2283
+ validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
2284
+ }
2285
+ };
2286
+ }
2287
+ return publicMetadataCors(Response.json(card, {
2288
+ headers: { "cache-control": "public, max-age=300" }
2289
+ }), request);
1839
2290
  }
1840
2291
  if (url.pathname === "/llms.txt" && surface === "hosted") {
1841
2292
  return request.method === "GET" ? publicText(`# TinyCloud MCP
@@ -1933,6 +2384,73 @@ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`
1933
2384
  );
1934
2385
  }
1935
2386
  }
2387
+ if (url.pathname === "/native/grant" && surface === "hosted") {
2388
+ if (request.method !== "POST") return methodNotAllowed("POST");
2389
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2390
+ if (config.nativeServiceCredential === void 0) {
2391
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2392
+ }
2393
+ if (!serviceCredentialMatches(
2394
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2395
+ config.nativeServiceCredential
2396
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2397
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
2398
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2399
+ }
2400
+ const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
2401
+ if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
2402
+ let body;
2403
+ try {
2404
+ body = JSON.parse(bodyText);
2405
+ } catch {
2406
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2407
+ }
2408
+ if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
2409
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2410
+ }
2411
+ const grantBody = body;
2412
+ if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
2413
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2414
+ }
2415
+ try {
2416
+ const accepted = await store.acceptNativeGrant(grantBody.delegation);
2417
+ return json({
2418
+ ownerDid: accepted.ownerDid,
2419
+ serviceDid: accepted.serviceDid,
2420
+ expiresAt: accepted.expiresAt
2421
+ }, 201);
2422
+ } catch (error) {
2423
+ return nativeGrantErrorResponse(error);
2424
+ }
2425
+ }
2426
+ if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
2427
+ if (config.nativeServiceCredential === void 0) {
2428
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2429
+ }
2430
+ if (!serviceCredentialMatches(
2431
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2432
+ config.nativeServiceCredential
2433
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2434
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2435
+ const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
2436
+ if (ownerHeader === null || ownerHeader.length === 0) {
2437
+ return json({ error: NATIVE_GRANT_REQUIRED }, 401);
2438
+ }
2439
+ const grant = await store.nativeGrantForOwner(ownerHeader);
2440
+ if (grant.status !== "ok") {
2441
+ const status = grant.status === "unavailable" ? 503 : 401;
2442
+ const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
2443
+ return json({ error: code }, status);
2444
+ }
2445
+ return handler.fetch(request, {
2446
+ authInfo: {
2447
+ token: "native-service",
2448
+ clientId: "tinycloud-native",
2449
+ scopes: [],
2450
+ extra: { nativeOwnerDid: grant.ownerDid }
2451
+ }
2452
+ });
2453
+ }
1936
2454
  if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
1937
2455
  const originFailure = validateOrigin(request, config.allowedOrigins);
1938
2456
  if (originFailure !== void 0) return originFailure;
@@ -2044,6 +2562,39 @@ function callbackCors(response, openkeyHost) {
2044
2562
  headers.set("vary", "Origin");
2045
2563
  return new Response(response.body, { status: response.status, headers });
2046
2564
  }
2565
+ async function readBoundedBodyText(request, maxBytes) {
2566
+ const contentLength = request.headers.get("content-length");
2567
+ if (contentLength !== null) {
2568
+ const parsedLength = Number(contentLength);
2569
+ if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
2570
+ }
2571
+ if (request.body === null) return "";
2572
+ const reader = request.body.getReader();
2573
+ const chunks = [];
2574
+ let total = 0;
2575
+ try {
2576
+ while (true) {
2577
+ const { done, value } = await reader.read();
2578
+ if (done) break;
2579
+ total += value.byteLength;
2580
+ if (total > maxBytes) {
2581
+ await reader.cancel();
2582
+ return void 0;
2583
+ }
2584
+ chunks.push(value);
2585
+ }
2586
+ } finally {
2587
+ reader.releaseLock();
2588
+ }
2589
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
2590
+ }
2591
+ function nativeGrantErrorResponse(error) {
2592
+ if (!(error instanceof NativeGrantValidationError)) {
2593
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2594
+ }
2595
+ const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
2596
+ return json({ error: error.code }, unavailable ? 503 : 401);
2597
+ }
2047
2598
  function json(value, status = 200) {
2048
2599
  return Response.json(value, { status, headers: { "cache-control": "no-store" } });
2049
2600
  }