@tinycloud/mcp 0.8.3-beta.15 → 0.8.3-beta.17

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