@tinycloud/mcp 0.7.0-beta.35 → 0.7.0-beta.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -440,6 +440,7 @@ function registerTinyCloudTools(server, selection, validator) {
440
440
  const metrics = { replayedGrants: 0 };
441
441
  let code = "INTERNAL_ERROR";
442
442
  try {
443
+ await selection.beforeOperation?.();
443
444
  const result = await (0, import_operations.invokeOperation)(
444
445
  binding.operationId,
445
446
  binding.operationVersion,
@@ -447,7 +448,11 @@ function registerTinyCloudTools(server, selection, validator) {
447
448
  input,
448
449
  metrics
449
450
  );
450
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
451
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result, {
452
+ operationId: binding.operationId,
453
+ operationVersion: binding.operationVersion,
454
+ input
455
+ });
451
456
  const response = toMcpToolResult(projected, TOOL_BINDINGS, {
452
457
  surface: selection.surface,
453
458
  readOnly: binding.readOnlyHint,
@@ -479,7 +484,7 @@ function outcomeCode(result) {
479
484
  // package.json
480
485
  var package_default = {
481
486
  name: "@tinycloud/mcp",
482
- version: "0.7.0-beta.35",
487
+ version: "0.7.0-beta.36",
483
488
  description: "TinyCloud delegated operations MCP server",
484
489
  keywords: [
485
490
  "tinycloud",
@@ -552,7 +557,7 @@ var package_default = {
552
557
  "@modelcontextprotocol/server": "2.0.0-beta.5",
553
558
  "@tinycloud/node-sdk": "3.3.0-beta.36",
554
559
  "@tinycloud/node-sdk-wasm": "1.7.8-beta.0",
555
- "@tinycloud/operations": "0.7.0-beta.34",
560
+ "@tinycloud/operations": "0.7.0-beta.35",
556
561
  jose: "6.2.4"
557
562
  },
558
563
  devDependencies: {
@@ -778,6 +783,7 @@ var import_node_path3 = require("path");
778
783
  var import_node_sdk2 = require("@tinycloud/node-sdk");
779
784
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
780
785
  var import_operations2 = require("@tinycloud/operations");
786
+ var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
781
787
  var import_tc_apps = require("@tinycloud/operations/tc-apps");
782
788
  var PROFILE = "agent";
783
789
  var MAX_CALLBACK_BYTES = 256 * 1024;
@@ -825,6 +831,15 @@ var ApprovalRequestError = class extends Error {
825
831
  var RemoteTenantStore = class {
826
832
  #config;
827
833
  #initializing = /* @__PURE__ */ new Map();
834
+ #sessionChecks = /* @__PURE__ */ new Map();
835
+ #clearing = /* @__PURE__ */ new Map();
836
+ #activeSessions = /* @__PURE__ */ new Map();
837
+ #pendingWrites = /* @__PURE__ */ new Map();
838
+ #renewalRequired = /* @__PURE__ */ new Set();
839
+ #approvalEpochs = /* @__PURE__ */ new Map();
840
+ #scopedClearing = /* @__PURE__ */ new Map();
841
+ #clearedScoped = /* @__PURE__ */ new Map();
842
+ #approvalProcessing = /* @__PURE__ */ new Map();
828
843
  constructor(config) {
829
844
  this.#config = config;
830
845
  }
@@ -835,9 +850,13 @@ var RemoteTenantStore = class {
835
850
  async initializeTenant(subject) {
836
851
  return (await this.#ensureTenant(subject)).stateRoot;
837
852
  }
853
+ /** Check stored node authority before an authenticated operation uses it. */
854
+ async validateTenant(subject, ownerDids) {
855
+ await this.#hasValidSession(await this.#ensureTenant(subject), ownerDids);
856
+ }
838
857
  async connectStatus(subject, ownerDids = []) {
839
858
  const tenant = await this.#ensureTenant(subject);
840
- if (await this.#hasValidSession(tenant, ownerDids)) {
859
+ if (await this.#hasValidSession(tenant, ownerDids, true)) {
841
860
  return { connected: true, sessionDid: tenant.sessionDid };
842
861
  }
843
862
  const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
@@ -848,24 +867,54 @@ var RemoteTenantStore = class {
848
867
  expiresAt: pending.expiresAt
849
868
  };
850
869
  }
851
- async decorateAuthorityResult(subject, ownerDids, result) {
870
+ async decorateAuthorityResult(subject, ownerDids, result, invocation) {
852
871
  if (!isRecord(result)) return result;
853
872
  if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
854
- await this.#ensureTenant(subject);
873
+ const tenant2 = await this.#ensureTenant(subject);
874
+ const refusal = revocationFromResult(result.error);
875
+ const cleared = refusal === void 0 ? void 0 : await this.#clearRefusedAuthority(tenant2, refusal);
876
+ if (cleared === "scoped" && invocation !== void 0) {
877
+ return this.#renewScopedApproval(subject, ownerDids, tenant2, result, invocation);
878
+ }
879
+ const valid = await this.#hasValidSession(tenant2, ownerDids, true);
880
+ if (valid && result.error.code === "AUTH_REQUIRED") {
881
+ if (refusal === void 0) return result;
882
+ return {
883
+ ...result,
884
+ error: { code: "AUTH_REQUIRED", message: "TinyCloud approval is required.", retryable: false }
885
+ };
886
+ }
855
887
  const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
856
888
  return {
857
- ...result,
889
+ status: "error",
890
+ operation: result.operation,
891
+ context: result.context,
858
892
  error: {
859
- ...result.error,
860
- details: {
861
- ...isRecord(result.error.details) ? result.error.details : {},
862
- approvalUrl: this.#connectUrl(pending2),
863
- expiresAt: pending2.expiresAt
864
- }
893
+ code: result.error.code === "SESSION_NOT_FOUND" && await readJson(this.#sessionPath(tenant2.stateRoot)) === null ? "SESSION_NOT_FOUND" : "AUTH_REQUIRED",
894
+ message: "A new TinyCloud approval is required.",
895
+ retryable: false,
896
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
865
897
  }
866
898
  };
867
899
  }
868
900
  if (result.status !== "authority_required") return result;
901
+ const tenant = await this.#ensureTenant(subject);
902
+ if (this.#renewalRequired.has(tenant.stateRoot) || !await this.#hasValidSession(tenant, ownerDids, true)) {
903
+ if (this.#renewalRequired.has(tenant.stateRoot)) {
904
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
905
+ return {
906
+ status: "error",
907
+ operation: result.operation,
908
+ context: result.context,
909
+ error: {
910
+ code: "AUTH_REQUIRED",
911
+ message: "A new TinyCloud approval is required.",
912
+ retryable: false,
913
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
914
+ }
915
+ };
916
+ }
917
+ }
869
918
  const request = isRecord(result.request) ? result.request : void 0;
870
919
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
871
920
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -882,6 +931,90 @@ var RemoteTenantStore = class {
882
931
  }
883
932
  };
884
933
  }
934
+ async #renewScopedApproval(subject, ownerDids, tenant, result, invocation) {
935
+ const planned = await (0, import_operations2.invokeOperation)(
936
+ "tinycloud.auth.request",
937
+ 1,
938
+ { profile: PROFILE, stateRoot: tenant.stateRoot },
939
+ invocation
940
+ );
941
+ const output = planned.status === "ok" && isRecord(planned.output) ? planned.output : void 0;
942
+ const request = isRecord(output?.request) ? output.request : void 0;
943
+ const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
944
+ const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
945
+ const missing = Array.isArray(output?.missing) ? output.missing.filter(isPermissionEntry) : [];
946
+ if (requestId === void 0 || requested.length === 0 || missing.length === 0) {
947
+ return {
948
+ status: "error",
949
+ operation: result.operation,
950
+ context: result.context,
951
+ error: { code: "AUTH_REQUIRED", message: "A new TinyCloud approval is required.", retryable: false }
952
+ };
953
+ }
954
+ const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
955
+ return {
956
+ status: "authority_required",
957
+ operation: result.operation,
958
+ context: result.context,
959
+ missing,
960
+ request,
961
+ approval: {
962
+ kind: "openkey",
963
+ requestId,
964
+ url: this.#connectUrl(pending),
965
+ fallback: (0, import_tc_apps.tcAppsApprovalMessage)(requested) ?? "Open the approval URL, then retry the same MCP tool."
966
+ },
967
+ retry: (0, import_mcp_authority.operationRetryDescriptor)(invocation, invocation.input)
968
+ };
969
+ }
970
+ async #clearRefusedAuthority(tenant, refusal) {
971
+ const raw = await readJson(this.#sessionPath(tenant.stateRoot)).catch(() => null);
972
+ const bootstrapCid = raw?.delegationCid;
973
+ const ancestorCid = refusal.reason === "delegation_ancestor_revoked" ? refusal.ancestorCid : void 0;
974
+ if (typeof bootstrapCid === "string" && (bootstrapCid === refusal.revokedCid || bootstrapCid === ancestorCid)) {
975
+ await this.#clearAuthority(tenant);
976
+ return "bootstrap";
977
+ }
978
+ const key = tenant.stateRoot;
979
+ const removed = this.#clearedScoped.get(tenant.stateRoot);
980
+ if (removed?.has(refusal.revokedCid)) return "scoped";
981
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
982
+ if (this.#clearedScoped.get(key)?.has(refusal.revokedCid)) return "scoped";
983
+ if (!Array.isArray(additional)) return void 0;
984
+ const affected = additional.some((entry) => {
985
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
986
+ return typeof cid === "string" && (cid === refusal.revokedCid || cid === ancestorCid);
987
+ });
988
+ const inProgress = this.#scopedClearing.get(key);
989
+ if (inProgress) {
990
+ await inProgress;
991
+ return this.#clearRefusedAuthority(tenant, refusal);
992
+ }
993
+ if (!affected) return void 0;
994
+ const clearing = (async () => {
995
+ const latest = await readJson(this.#additionalPath(tenant.stateRoot));
996
+ if (!Array.isArray(latest)) return;
997
+ const remaining = latest.filter((entry) => {
998
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
999
+ return typeof cid !== "string" || cid !== refusal.revokedCid && cid !== ancestorCid;
1000
+ });
1001
+ if (remaining.length === latest.length) return;
1002
+ this.#activeSessions.delete(tenant.stateRoot);
1003
+ const epoch = (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1;
1004
+ this.#approvalEpochs.set(tenant.stateRoot, epoch);
1005
+ const profilePath = this.#profilePath(tenant.stateRoot);
1006
+ const profile = await readJson(profilePath);
1007
+ await secureWrite(profilePath, { ...profile, approvalEpoch: epoch });
1008
+ if (remaining.length === 0) await (0, import_promises.rm)(this.#additionalPath(tenant.stateRoot), { force: true });
1009
+ else await secureWrite(this.#additionalPath(tenant.stateRoot), remaining);
1010
+ const cleared = removed ?? /* @__PURE__ */ new Set();
1011
+ cleared.add(refusal.revokedCid);
1012
+ this.#clearedScoped.set(tenant.stateRoot, cleared);
1013
+ })().finally(() => this.#scopedClearing.delete(key));
1014
+ this.#scopedClearing.set(key, clearing);
1015
+ await clearing;
1016
+ return "scoped";
1017
+ }
885
1018
  async approvalRedirect(state) {
886
1019
  const pending = await this.#readPending(state, false);
887
1020
  const tenant = await this.#readTenant(pending.tenantId);
@@ -922,21 +1055,36 @@ var RemoteTenantStore = class {
922
1055
  }
923
1056
  if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
924
1057
  const pending = await this.#readPending(state, true);
925
- try {
926
- const tenant = await this.#readTenant(pending.tenantId);
927
- if (pending.kind === "bootstrap") {
928
- await this.#storeBootstrap(tenant, pending, data);
929
- } else {
930
- await this.#storeDelegation(tenant, pending, data);
1058
+ const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", pending.tenantId);
1059
+ const prior = this.#approvalProcessing.get(stateRoot);
1060
+ const processing = (async () => {
1061
+ if (prior) await prior.catch(() => void 0);
1062
+ try {
1063
+ const tenant = await this.#readTenant(pending.tenantId);
1064
+ if ((pending.approvalEpoch ?? 0) !== tenant.approvalEpoch) {
1065
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
1066
+ }
1067
+ if (pending.kind === "bootstrap") {
1068
+ await this.#storeBootstrap(tenant, pending, data);
1069
+ } else {
1070
+ await this.#storeDelegation(tenant, pending, data);
1071
+ }
1072
+ await (0, import_promises.rm)(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
1073
+ } catch (error) {
1074
+ await this.#restorePending(pending).catch(() => void 0);
1075
+ throw error;
931
1076
  }
932
- await (0, import_promises.rm)(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
933
- } catch (error) {
934
- await this.#restorePending(pending).catch(() => void 0);
935
- throw error;
1077
+ })();
1078
+ this.#approvalProcessing.set(stateRoot, processing);
1079
+ try {
1080
+ await processing;
1081
+ } finally {
1082
+ if (this.#approvalProcessing.get(stateRoot) === processing) this.#approvalProcessing.delete(stateRoot);
936
1083
  }
937
1084
  }
938
1085
  async #storeBootstrap(tenant, pending, data) {
939
1086
  const { node, session } = await this.#restoreApproval(tenant, data);
1087
+ this.#assertCurrentApproval(pending, session);
940
1088
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
941
1089
  throw new ApprovalOwnerMismatchError();
942
1090
  }
@@ -953,10 +1101,13 @@ var RemoteTenantStore = class {
953
1101
  authMethod: "openkey",
954
1102
  ...session
955
1103
  });
1104
+ this.#renewalRequired.delete(tenant.stateRoot);
1105
+ this.#activeSessions.delete(tenant.stateRoot);
956
1106
  }
957
1107
  async #storeDelegation(tenant, pending, data) {
958
1108
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
959
1109
  const { node, session } = await this.#restoreApproval(tenant, data);
1110
+ this.#assertCurrentApproval(pending, session);
960
1111
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
961
1112
  let delegation;
962
1113
  try {
@@ -1000,6 +1151,12 @@ var RemoteTenantStore = class {
1000
1151
  }
1001
1152
  if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
1002
1153
  }
1154
+ #assertCurrentApproval(pending, session) {
1155
+ const issuedAt = /^Issued At: ([^\r\n]+)\r?$/m.exec(session.siwe)?.[1];
1156
+ if (issuedAt === void 0 || !Number.isFinite(Date.parse(issuedAt)) || Date.parse(issuedAt) < Date.parse(pending.createdAt)) {
1157
+ throw new ApprovalStageError("restore_approval", "APPROVAL_PREDATES_REQUEST");
1158
+ }
1159
+ }
1003
1160
  /**
1004
1161
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
1005
1162
  * callback must carry the full signed proof (SIWE, signature, address and
@@ -1040,17 +1197,104 @@ var RemoteTenantStore = class {
1040
1197
  this.#initializing.set(tenantId, initializing);
1041
1198
  return initializing;
1042
1199
  }
1043
- async #hasValidSession(tenant, ownerDids) {
1044
- const raw = await readJson(this.#sessionPath(tenant.stateRoot));
1200
+ async #hasValidSession(tenant, ownerDids, fresh = false) {
1201
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
1202
+ const key = JSON.stringify([tenant.stateRoot, ownerDids.map(normalizeDid), fresh]);
1203
+ const current = this.#sessionChecks.get(key);
1204
+ if (current) return current;
1205
+ const checking = this.#checkSession(tenant, ownerDids, fresh).finally(() => {
1206
+ this.#sessionChecks.delete(key);
1207
+ });
1208
+ this.#sessionChecks.set(key, checking);
1209
+ return checking;
1210
+ }
1211
+ async #checkSession(tenant, ownerDids, fresh) {
1212
+ let raw;
1213
+ try {
1214
+ raw = await readJson(this.#sessionPath(tenant.stateRoot));
1215
+ } catch (error) {
1216
+ if (!(error instanceof SyntaxError)) throw error;
1217
+ await this.#clearAuthority(tenant);
1218
+ return false;
1219
+ }
1045
1220
  if (raw === null) return false;
1221
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
1046
1222
  try {
1047
- const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
1048
1223
  await node.restoreSession(raw);
1049
- return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
1050
1224
  } catch {
1051
- await (0, import_promises.rm)(this.#sessionPath(tenant.stateRoot), { force: true });
1225
+ const header = isRecord(raw.delegationHeader) ? raw.delegationHeader.Authorization : void 0;
1226
+ const signedExpiry = typeof header === "string" ? (0, import_node_sdk2.signedCompactUcanExpiry)(header) : void 0;
1227
+ const siweExpiry = typeof raw.siwe === "string" ? /^Expiration Time: ([^\r\n]+)\r?$/m.exec(raw.siwe)?.[1] : void 0;
1228
+ const expiry = signedExpiry ?? (siweExpiry === void 0 ? void 0 : new Date(siweExpiry));
1229
+ if (expiry !== void 0 && expiry.getTime() <= Date.now()) return false;
1230
+ await this.#clearAuthority(tenant);
1231
+ return false;
1232
+ }
1233
+ if (!samePrincipal(node.sessionDid, tenant.sessionDid) || !ownerDids.some((did) => samePrincipal(node.did, did))) return false;
1234
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
1235
+ const cids = [raw.delegationCid];
1236
+ if (additional !== null) {
1237
+ if (!Array.isArray(additional)) throw new Error("Stored TinyCloud authority is unavailable.");
1238
+ for (const entry of additional) {
1239
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
1240
+ cids.push(cid);
1241
+ }
1242
+ }
1243
+ if (!cids.every((cid) => typeof cid === "string" && cid.length > 0)) {
1244
+ throw new Error("Stored TinyCloud authority is unavailable.");
1245
+ }
1246
+ const fingerprint = cids.join("\n");
1247
+ const cached = this.#activeSessions.get(tenant.stateRoot);
1248
+ if (!fresh && cached?.cids === fingerprint && cached.until > Date.now()) return true;
1249
+ let timer;
1250
+ const timeout = new Promise((resolve) => {
1251
+ timer = setTimeout(() => resolve(void 0), 2e3);
1252
+ timer.unref();
1253
+ });
1254
+ const probe = await Promise.race([
1255
+ node.kvForSpace(String(raw.spaceId)).list({ prefix: "spaces/" }).catch(() => void 0),
1256
+ timeout
1257
+ ]).finally(() => clearTimeout(timer));
1258
+ const refusal = probe !== void 0 && !probe.ok ? (0, import_mcp_authority.nodeRevocationOf)(probe.error) : void 0;
1259
+ if (refusal !== void 0 && (refusal.revokedCid === raw.delegationCid || refusal.reason === "delegation_ancestor_revoked" && refusal.ancestorCid === raw.delegationCid)) {
1260
+ await this.#clearAuthority(tenant);
1052
1261
  return false;
1053
1262
  }
1263
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
1264
+ if (probe?.ok) {
1265
+ this.#activeSessions.set(tenant.stateRoot, { cids: fingerprint, until: Date.now() + 3e4 });
1266
+ }
1267
+ return true;
1268
+ }
1269
+ async #clearAuthority(tenant) {
1270
+ const current = this.#clearing.get(tenant.stateRoot);
1271
+ if (current) return current;
1272
+ this.#activeSessions.delete(tenant.stateRoot);
1273
+ this.#renewalRequired.add(tenant.stateRoot);
1274
+ this.#clearedScoped.delete(tenant.stateRoot);
1275
+ this.#approvalEpochs.set(
1276
+ tenant.stateRoot,
1277
+ (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1
1278
+ );
1279
+ const clearing = (async () => {
1280
+ const processing = this.#approvalProcessing.get(tenant.stateRoot);
1281
+ if (processing) await processing.catch(() => void 0);
1282
+ const scoped = this.#scopedClearing.get(tenant.stateRoot);
1283
+ if (scoped) await scoped;
1284
+ this.#renewalRequired.add(tenant.stateRoot);
1285
+ const profilePath = this.#profilePath(tenant.stateRoot);
1286
+ const profile = await readJson(profilePath);
1287
+ await secureWrite(profilePath, {
1288
+ ...profile,
1289
+ approvalEpoch: this.#approvalEpochs.get(tenant.stateRoot)
1290
+ });
1291
+ await (0, import_promises.rm)(this.#additionalPath(tenant.stateRoot), { force: true });
1292
+ await (0, import_promises.rm)((0, import_node_path3.join)(tenant.stateRoot, ".tinycloud", "profiles", PROFILE, "auth-requests.json"), { force: true });
1293
+ await (0, import_promises.rm)(this.#pendingDirectory(tenant.stateRoot), { recursive: true, force: true });
1294
+ await (0, import_promises.rm)(this.#sessionPath(tenant.stateRoot), { force: true });
1295
+ })().finally(() => this.#clearing.delete(tenant.stateRoot));
1296
+ this.#clearing.set(tenant.stateRoot, clearing);
1297
+ await clearing;
1054
1298
  }
1055
1299
  async #initializeTenant(tenantId) {
1056
1300
  const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
@@ -1089,21 +1333,36 @@ var RemoteTenantStore = class {
1089
1333
  `
1090
1334
  );
1091
1335
  }
1092
- return { stateRoot, sessionDid, jwk };
1336
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
1337
+ this.#approvalEpochs.set(stateRoot, approvalEpoch);
1338
+ return { stateRoot, sessionDid, jwk, approvalEpoch };
1093
1339
  }
1094
1340
  async #readTenant(tenantId) {
1095
1341
  if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1096
1342
  const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
1097
1343
  const jwk = await readJson(this.#keyPath(stateRoot));
1098
1344
  if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
1099
- return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
1345
+ const profile = await readJson(this.#profilePath(stateRoot));
1346
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
1347
+ return { stateRoot, jwk, sessionDid: didFromJwk(jwk), approvalEpoch };
1100
1348
  }
1101
1349
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
1350
+ const clearing = this.#clearing.get(this.tenantStateRoot(subject));
1351
+ if (clearing) await clearing;
1102
1352
  const tenant = await this.#ensureTenant(subject);
1103
1353
  const tenantId = this.#tenantId(subject);
1354
+ const normalizedOwners = ownerDids.map(normalizeDid);
1355
+ const key = JSON.stringify([tenantId, tenant.approvalEpoch, kind, requestId, permissions, normalizedOwners]);
1356
+ const current = this.#pendingWrites.get(key);
1357
+ if (current) return current;
1358
+ const writing = this.#writePending(tenant, tenantId, kind, permissions, normalizedOwners, requestId).finally(() => this.#pendingWrites.delete(key));
1359
+ this.#pendingWrites.set(key, writing);
1360
+ return writing;
1361
+ }
1362
+ async #writePending(tenant, tenantId, kind, permissions, ownerDids, requestId) {
1104
1363
  const reusable = await reusablePending(
1105
1364
  this.#pendingDirectory(tenant.stateRoot),
1106
- { tenantId, kind, requestId, permissions, ownerDids: ownerDids.map(normalizeDid) }
1365
+ { tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
1107
1366
  );
1108
1367
  if (reusable !== void 0) return reusable;
1109
1368
  const nonce = (0, import_node_crypto.randomBytes)(24).toString("base64url");
@@ -1112,10 +1371,11 @@ var RemoteTenantStore = class {
1112
1371
  version: 1,
1113
1372
  nonce,
1114
1373
  tenantId,
1374
+ approvalEpoch: tenant.approvalEpoch,
1115
1375
  kind,
1116
1376
  ...requestId === void 0 ? {} : { requestId },
1117
1377
  permissions,
1118
- ownerDids: ownerDids.map(normalizeDid),
1378
+ ownerDids,
1119
1379
  createdAt: new Date(created).toISOString(),
1120
1380
  expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
1121
1381
  };
@@ -1156,6 +1416,11 @@ var RemoteTenantStore = class {
1156
1416
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
1157
1417
  throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
1158
1418
  }
1419
+ const profile = await readJson(this.#profilePath(stateRoot));
1420
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
1421
+ if ((pending.approvalEpoch ?? 0) !== approvalEpoch) {
1422
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
1423
+ }
1159
1424
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
1160
1425
  await (0, import_promises.rm)(consume ? claimed : path, { force: true });
1161
1426
  throw new ApprovalRequestError("state_expired", "Approval state has expired.");
@@ -1191,6 +1456,9 @@ var RemoteTenantStore = class {
1191
1456
  #sessionPath(stateRoot) {
1192
1457
  return (0, import_node_path3.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
1193
1458
  }
1459
+ #additionalPath(stateRoot) {
1460
+ return (0, import_node_path3.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "additional-delegations.json");
1461
+ }
1194
1462
  #pendingPath(stateRoot, nonce) {
1195
1463
  return (0, import_node_path3.join)(this.#pendingDirectory(stateRoot), `${nonce}.json`);
1196
1464
  }
@@ -1218,6 +1486,21 @@ function ensureWasm() {
1218
1486
  (0, import_node_sdk_wasm.initPanicHook)();
1219
1487
  wasmInitialized = true;
1220
1488
  }
1489
+ function revocationFromResult(error) {
1490
+ const details = error.details;
1491
+ if (!isRecord(details) || typeof details.revokedCid !== "string") return void 0;
1492
+ if (details.reason === "delegation_revoked") {
1493
+ return { reason: "delegation_revoked", revokedCid: details.revokedCid };
1494
+ }
1495
+ if (details.reason === "delegation_ancestor_revoked" && typeof details.ancestorCid === "string") {
1496
+ return {
1497
+ reason: "delegation_ancestor_revoked",
1498
+ revokedCid: details.revokedCid,
1499
+ ancestorCid: details.ancestorCid
1500
+ };
1501
+ }
1502
+ return void 0;
1503
+ }
1221
1504
  function sessionFromCallback(data, privateJwk) {
1222
1505
  const delegationHeader = data.delegationHeader;
1223
1506
  if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string" || typeof data.address !== "string" || typeof data.chainId !== "number" || typeof data.siwe !== "string" || typeof data.signature !== "string") {
@@ -1308,7 +1591,7 @@ async function reusablePending(directory, expected) {
1308
1591
  return void 0;
1309
1592
  }
1310
1593
  function samePendingRequest(pending, expected) {
1311
- return pending.version === 1 && pending.tenantId === expected.tenantId && pending.kind === expected.kind && pending.requestId === expected.requestId && JSON.stringify(pending.permissions) === JSON.stringify(expected.permissions) && JSON.stringify(pending.ownerDids) === JSON.stringify(expected.ownerDids);
1594
+ return pending.version === 1 && pending.tenantId === expected.tenantId && (pending.approvalEpoch ?? 0) === expected.approvalEpoch && pending.kind === expected.kind && pending.requestId === expected.requestId && JSON.stringify(pending.permissions) === JSON.stringify(expected.permissions) && JSON.stringify(pending.ownerDids) === JSON.stringify(expected.ownerDids);
1312
1595
  }
1313
1596
 
1314
1597
  // src/http.ts
@@ -1348,7 +1631,8 @@ function createHostedMcpApp(config) {
1348
1631
  explicitProfile: true,
1349
1632
  allowOwnerProfile: false,
1350
1633
  stateRoot,
1351
- transformOperationResult: (result) => store.decorateAuthorityResult(subject, ownerDids, result)
1634
+ beforeOperation: () => store.validateTenant(subject, ownerDids),
1635
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1352
1636
  }, validator);
1353
1637
  registerConnectTool(server, validator, store, identity);
1354
1638
  return server;