@tinycloud/mcp 0.7.0-beta.34 → 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.js CHANGED
@@ -403,6 +403,7 @@ function registerTinyCloudTools(server, selection, validator) {
403
403
  const metrics = { replayedGrants: 0 };
404
404
  let code = "INTERNAL_ERROR";
405
405
  try {
406
+ await selection.beforeOperation?.();
406
407
  const result = await invokeOperation(
407
408
  binding.operationId,
408
409
  binding.operationVersion,
@@ -410,7 +411,11 @@ function registerTinyCloudTools(server, selection, validator) {
410
411
  input,
411
412
  metrics
412
413
  );
413
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
414
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result, {
415
+ operationId: binding.operationId,
416
+ operationVersion: binding.operationVersion,
417
+ input
418
+ });
414
419
  const response = toMcpToolResult(projected, TOOL_BINDINGS, {
415
420
  surface: selection.surface,
416
421
  readOnly: binding.readOnlyHint,
@@ -442,7 +447,7 @@ function outcomeCode(result) {
442
447
  // package.json
443
448
  var package_default = {
444
449
  name: "@tinycloud/mcp",
445
- version: "0.7.0-beta.34",
450
+ version: "0.7.0-beta.36",
446
451
  description: "TinyCloud delegated operations MCP server",
447
452
  keywords: [
448
453
  "tinycloud",
@@ -513,9 +518,9 @@ var package_default = {
513
518
  dependencies: {
514
519
  "@modelcontextprotocol/node": "2.0.0-beta.5",
515
520
  "@modelcontextprotocol/server": "2.0.0-beta.5",
516
- "@tinycloud/node-sdk": "3.3.0-beta.35",
521
+ "@tinycloud/node-sdk": "3.3.0-beta.36",
517
522
  "@tinycloud/node-sdk-wasm": "1.7.8-beta.0",
518
- "@tinycloud/operations": "0.7.0-beta.33",
523
+ "@tinycloud/operations": "0.7.0-beta.35",
519
524
  jose: "6.2.4"
520
525
  },
521
526
  devDependencies: {
@@ -756,10 +761,12 @@ import {
756
761
  UnsupportedSessionRestoreError,
757
762
  isCapabilitySubset,
758
763
  principalDidEquals,
759
- siweRuntimeDelegation
764
+ siweRuntimeDelegation,
765
+ signedCompactUcanExpiry
760
766
  } from "@tinycloud/node-sdk";
761
767
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
762
768
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
769
+ import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
763
770
  import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
764
771
  var PROFILE = "agent";
765
772
  var MAX_CALLBACK_BYTES = 256 * 1024;
@@ -807,6 +814,15 @@ var ApprovalRequestError = class extends Error {
807
814
  var RemoteTenantStore = class {
808
815
  #config;
809
816
  #initializing = /* @__PURE__ */ new Map();
817
+ #sessionChecks = /* @__PURE__ */ new Map();
818
+ #clearing = /* @__PURE__ */ new Map();
819
+ #activeSessions = /* @__PURE__ */ new Map();
820
+ #pendingWrites = /* @__PURE__ */ new Map();
821
+ #renewalRequired = /* @__PURE__ */ new Set();
822
+ #approvalEpochs = /* @__PURE__ */ new Map();
823
+ #scopedClearing = /* @__PURE__ */ new Map();
824
+ #clearedScoped = /* @__PURE__ */ new Map();
825
+ #approvalProcessing = /* @__PURE__ */ new Map();
810
826
  constructor(config) {
811
827
  this.#config = config;
812
828
  }
@@ -817,9 +833,13 @@ var RemoteTenantStore = class {
817
833
  async initializeTenant(subject) {
818
834
  return (await this.#ensureTenant(subject)).stateRoot;
819
835
  }
836
+ /** Check stored node authority before an authenticated operation uses it. */
837
+ async validateTenant(subject, ownerDids) {
838
+ await this.#hasValidSession(await this.#ensureTenant(subject), ownerDids);
839
+ }
820
840
  async connectStatus(subject, ownerDids = []) {
821
841
  const tenant = await this.#ensureTenant(subject);
822
- if (await this.#hasValidSession(tenant, ownerDids)) {
842
+ if (await this.#hasValidSession(tenant, ownerDids, true)) {
823
843
  return { connected: true, sessionDid: tenant.sessionDid };
824
844
  }
825
845
  const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
@@ -830,24 +850,54 @@ var RemoteTenantStore = class {
830
850
  expiresAt: pending.expiresAt
831
851
  };
832
852
  }
833
- async decorateAuthorityResult(subject, ownerDids, result) {
853
+ async decorateAuthorityResult(subject, ownerDids, result, invocation) {
834
854
  if (!isRecord(result)) return result;
835
855
  if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
836
- await this.#ensureTenant(subject);
856
+ const tenant2 = await this.#ensureTenant(subject);
857
+ const refusal = revocationFromResult(result.error);
858
+ const cleared = refusal === void 0 ? void 0 : await this.#clearRefusedAuthority(tenant2, refusal);
859
+ if (cleared === "scoped" && invocation !== void 0) {
860
+ return this.#renewScopedApproval(subject, ownerDids, tenant2, result, invocation);
861
+ }
862
+ const valid = await this.#hasValidSession(tenant2, ownerDids, true);
863
+ if (valid && result.error.code === "AUTH_REQUIRED") {
864
+ if (refusal === void 0) return result;
865
+ return {
866
+ ...result,
867
+ error: { code: "AUTH_REQUIRED", message: "TinyCloud approval is required.", retryable: false }
868
+ };
869
+ }
837
870
  const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
838
871
  return {
839
- ...result,
872
+ status: "error",
873
+ operation: result.operation,
874
+ context: result.context,
840
875
  error: {
841
- ...result.error,
842
- details: {
843
- ...isRecord(result.error.details) ? result.error.details : {},
844
- approvalUrl: this.#connectUrl(pending2),
845
- expiresAt: pending2.expiresAt
846
- }
876
+ code: result.error.code === "SESSION_NOT_FOUND" && await readJson(this.#sessionPath(tenant2.stateRoot)) === null ? "SESSION_NOT_FOUND" : "AUTH_REQUIRED",
877
+ message: "A new TinyCloud approval is required.",
878
+ retryable: false,
879
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
847
880
  }
848
881
  };
849
882
  }
850
883
  if (result.status !== "authority_required") return result;
884
+ const tenant = await this.#ensureTenant(subject);
885
+ if (this.#renewalRequired.has(tenant.stateRoot) || !await this.#hasValidSession(tenant, ownerDids, true)) {
886
+ if (this.#renewalRequired.has(tenant.stateRoot)) {
887
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
888
+ return {
889
+ status: "error",
890
+ operation: result.operation,
891
+ context: result.context,
892
+ error: {
893
+ code: "AUTH_REQUIRED",
894
+ message: "A new TinyCloud approval is required.",
895
+ retryable: false,
896
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
897
+ }
898
+ };
899
+ }
900
+ }
851
901
  const request = isRecord(result.request) ? result.request : void 0;
852
902
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
853
903
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -864,6 +914,90 @@ var RemoteTenantStore = class {
864
914
  }
865
915
  };
866
916
  }
917
+ async #renewScopedApproval(subject, ownerDids, tenant, result, invocation) {
918
+ const planned = await invokeOperation2(
919
+ "tinycloud.auth.request",
920
+ 1,
921
+ { profile: PROFILE, stateRoot: tenant.stateRoot },
922
+ invocation
923
+ );
924
+ const output = planned.status === "ok" && isRecord(planned.output) ? planned.output : void 0;
925
+ const request = isRecord(output?.request) ? output.request : void 0;
926
+ const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
927
+ const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
928
+ const missing = Array.isArray(output?.missing) ? output.missing.filter(isPermissionEntry) : [];
929
+ if (requestId === void 0 || requested.length === 0 || missing.length === 0) {
930
+ return {
931
+ status: "error",
932
+ operation: result.operation,
933
+ context: result.context,
934
+ error: { code: "AUTH_REQUIRED", message: "A new TinyCloud approval is required.", retryable: false }
935
+ };
936
+ }
937
+ const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
938
+ return {
939
+ status: "authority_required",
940
+ operation: result.operation,
941
+ context: result.context,
942
+ missing,
943
+ request,
944
+ approval: {
945
+ kind: "openkey",
946
+ requestId,
947
+ url: this.#connectUrl(pending),
948
+ fallback: tcAppsApprovalMessage(requested) ?? "Open the approval URL, then retry the same MCP tool."
949
+ },
950
+ retry: operationRetryDescriptor(invocation, invocation.input)
951
+ };
952
+ }
953
+ async #clearRefusedAuthority(tenant, refusal) {
954
+ const raw = await readJson(this.#sessionPath(tenant.stateRoot)).catch(() => null);
955
+ const bootstrapCid = raw?.delegationCid;
956
+ const ancestorCid = refusal.reason === "delegation_ancestor_revoked" ? refusal.ancestorCid : void 0;
957
+ if (typeof bootstrapCid === "string" && (bootstrapCid === refusal.revokedCid || bootstrapCid === ancestorCid)) {
958
+ await this.#clearAuthority(tenant);
959
+ return "bootstrap";
960
+ }
961
+ const key = tenant.stateRoot;
962
+ const removed = this.#clearedScoped.get(tenant.stateRoot);
963
+ if (removed?.has(refusal.revokedCid)) return "scoped";
964
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
965
+ if (this.#clearedScoped.get(key)?.has(refusal.revokedCid)) return "scoped";
966
+ if (!Array.isArray(additional)) return void 0;
967
+ const affected = additional.some((entry) => {
968
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
969
+ return typeof cid === "string" && (cid === refusal.revokedCid || cid === ancestorCid);
970
+ });
971
+ const inProgress = this.#scopedClearing.get(key);
972
+ if (inProgress) {
973
+ await inProgress;
974
+ return this.#clearRefusedAuthority(tenant, refusal);
975
+ }
976
+ if (!affected) return void 0;
977
+ const clearing = (async () => {
978
+ const latest = await readJson(this.#additionalPath(tenant.stateRoot));
979
+ if (!Array.isArray(latest)) return;
980
+ const remaining = latest.filter((entry) => {
981
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
982
+ return typeof cid !== "string" || cid !== refusal.revokedCid && cid !== ancestorCid;
983
+ });
984
+ if (remaining.length === latest.length) return;
985
+ this.#activeSessions.delete(tenant.stateRoot);
986
+ const epoch = (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1;
987
+ this.#approvalEpochs.set(tenant.stateRoot, epoch);
988
+ const profilePath = this.#profilePath(tenant.stateRoot);
989
+ const profile = await readJson(profilePath);
990
+ await secureWrite(profilePath, { ...profile, approvalEpoch: epoch });
991
+ if (remaining.length === 0) await rm(this.#additionalPath(tenant.stateRoot), { force: true });
992
+ else await secureWrite(this.#additionalPath(tenant.stateRoot), remaining);
993
+ const cleared = removed ?? /* @__PURE__ */ new Set();
994
+ cleared.add(refusal.revokedCid);
995
+ this.#clearedScoped.set(tenant.stateRoot, cleared);
996
+ })().finally(() => this.#scopedClearing.delete(key));
997
+ this.#scopedClearing.set(key, clearing);
998
+ await clearing;
999
+ return "scoped";
1000
+ }
867
1001
  async approvalRedirect(state) {
868
1002
  const pending = await this.#readPending(state, false);
869
1003
  const tenant = await this.#readTenant(pending.tenantId);
@@ -904,21 +1038,36 @@ var RemoteTenantStore = class {
904
1038
  }
905
1039
  if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
906
1040
  const pending = await this.#readPending(state, true);
907
- try {
908
- const tenant = await this.#readTenant(pending.tenantId);
909
- if (pending.kind === "bootstrap") {
910
- await this.#storeBootstrap(tenant, pending, data);
911
- } else {
912
- await this.#storeDelegation(tenant, pending, data);
1041
+ const stateRoot = join2(this.#config.stateDir, "tenants", pending.tenantId);
1042
+ const prior = this.#approvalProcessing.get(stateRoot);
1043
+ const processing = (async () => {
1044
+ if (prior) await prior.catch(() => void 0);
1045
+ try {
1046
+ const tenant = await this.#readTenant(pending.tenantId);
1047
+ if ((pending.approvalEpoch ?? 0) !== tenant.approvalEpoch) {
1048
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
1049
+ }
1050
+ if (pending.kind === "bootstrap") {
1051
+ await this.#storeBootstrap(tenant, pending, data);
1052
+ } else {
1053
+ await this.#storeDelegation(tenant, pending, data);
1054
+ }
1055
+ await rm(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
1056
+ } catch (error) {
1057
+ await this.#restorePending(pending).catch(() => void 0);
1058
+ throw error;
913
1059
  }
914
- await rm(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
915
- } catch (error) {
916
- await this.#restorePending(pending).catch(() => void 0);
917
- throw error;
1060
+ })();
1061
+ this.#approvalProcessing.set(stateRoot, processing);
1062
+ try {
1063
+ await processing;
1064
+ } finally {
1065
+ if (this.#approvalProcessing.get(stateRoot) === processing) this.#approvalProcessing.delete(stateRoot);
918
1066
  }
919
1067
  }
920
1068
  async #storeBootstrap(tenant, pending, data) {
921
1069
  const { node, session } = await this.#restoreApproval(tenant, data);
1070
+ this.#assertCurrentApproval(pending, session);
922
1071
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
923
1072
  throw new ApprovalOwnerMismatchError();
924
1073
  }
@@ -935,10 +1084,13 @@ var RemoteTenantStore = class {
935
1084
  authMethod: "openkey",
936
1085
  ...session
937
1086
  });
1087
+ this.#renewalRequired.delete(tenant.stateRoot);
1088
+ this.#activeSessions.delete(tenant.stateRoot);
938
1089
  }
939
1090
  async #storeDelegation(tenant, pending, data) {
940
1091
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
941
1092
  const { node, session } = await this.#restoreApproval(tenant, data);
1093
+ this.#assertCurrentApproval(pending, session);
942
1094
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
943
1095
  let delegation;
944
1096
  try {
@@ -982,6 +1134,12 @@ var RemoteTenantStore = class {
982
1134
  }
983
1135
  if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
984
1136
  }
1137
+ #assertCurrentApproval(pending, session) {
1138
+ const issuedAt = /^Issued At: ([^\r\n]+)\r?$/m.exec(session.siwe)?.[1];
1139
+ if (issuedAt === void 0 || !Number.isFinite(Date.parse(issuedAt)) || Date.parse(issuedAt) < Date.parse(pending.createdAt)) {
1140
+ throw new ApprovalStageError("restore_approval", "APPROVAL_PREDATES_REQUEST");
1141
+ }
1142
+ }
985
1143
  /**
986
1144
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
987
1145
  * callback must carry the full signed proof (SIWE, signature, address and
@@ -1022,17 +1180,104 @@ var RemoteTenantStore = class {
1022
1180
  this.#initializing.set(tenantId, initializing);
1023
1181
  return initializing;
1024
1182
  }
1025
- async #hasValidSession(tenant, ownerDids) {
1026
- const raw = await readJson(this.#sessionPath(tenant.stateRoot));
1183
+ async #hasValidSession(tenant, ownerDids, fresh = false) {
1184
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
1185
+ const key = JSON.stringify([tenant.stateRoot, ownerDids.map(normalizeDid), fresh]);
1186
+ const current = this.#sessionChecks.get(key);
1187
+ if (current) return current;
1188
+ const checking = this.#checkSession(tenant, ownerDids, fresh).finally(() => {
1189
+ this.#sessionChecks.delete(key);
1190
+ });
1191
+ this.#sessionChecks.set(key, checking);
1192
+ return checking;
1193
+ }
1194
+ async #checkSession(tenant, ownerDids, fresh) {
1195
+ let raw;
1196
+ try {
1197
+ raw = await readJson(this.#sessionPath(tenant.stateRoot));
1198
+ } catch (error) {
1199
+ if (!(error instanceof SyntaxError)) throw error;
1200
+ await this.#clearAuthority(tenant);
1201
+ return false;
1202
+ }
1027
1203
  if (raw === null) return false;
1204
+ const node = new TinyCloudNode({ host: this.#config.nodeHost });
1028
1205
  try {
1029
- const node = new TinyCloudNode({ host: this.#config.nodeHost });
1030
1206
  await node.restoreSession(raw);
1031
- return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
1032
1207
  } catch {
1033
- await rm(this.#sessionPath(tenant.stateRoot), { force: true });
1208
+ const header = isRecord(raw.delegationHeader) ? raw.delegationHeader.Authorization : void 0;
1209
+ const signedExpiry = typeof header === "string" ? signedCompactUcanExpiry(header) : void 0;
1210
+ const siweExpiry = typeof raw.siwe === "string" ? /^Expiration Time: ([^\r\n]+)\r?$/m.exec(raw.siwe)?.[1] : void 0;
1211
+ const expiry = signedExpiry ?? (siweExpiry === void 0 ? void 0 : new Date(siweExpiry));
1212
+ if (expiry !== void 0 && expiry.getTime() <= Date.now()) return false;
1213
+ await this.#clearAuthority(tenant);
1214
+ return false;
1215
+ }
1216
+ if (!samePrincipal(node.sessionDid, tenant.sessionDid) || !ownerDids.some((did) => samePrincipal(node.did, did))) return false;
1217
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
1218
+ const cids = [raw.delegationCid];
1219
+ if (additional !== null) {
1220
+ if (!Array.isArray(additional)) throw new Error("Stored TinyCloud authority is unavailable.");
1221
+ for (const entry of additional) {
1222
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
1223
+ cids.push(cid);
1224
+ }
1225
+ }
1226
+ if (!cids.every((cid) => typeof cid === "string" && cid.length > 0)) {
1227
+ throw new Error("Stored TinyCloud authority is unavailable.");
1228
+ }
1229
+ const fingerprint = cids.join("\n");
1230
+ const cached = this.#activeSessions.get(tenant.stateRoot);
1231
+ if (!fresh && cached?.cids === fingerprint && cached.until > Date.now()) return true;
1232
+ let timer;
1233
+ const timeout = new Promise((resolve) => {
1234
+ timer = setTimeout(() => resolve(void 0), 2e3);
1235
+ timer.unref();
1236
+ });
1237
+ const probe = await Promise.race([
1238
+ node.kvForSpace(String(raw.spaceId)).list({ prefix: "spaces/" }).catch(() => void 0),
1239
+ timeout
1240
+ ]).finally(() => clearTimeout(timer));
1241
+ const refusal = probe !== void 0 && !probe.ok ? nodeRevocationOf(probe.error) : void 0;
1242
+ if (refusal !== void 0 && (refusal.revokedCid === raw.delegationCid || refusal.reason === "delegation_ancestor_revoked" && refusal.ancestorCid === raw.delegationCid)) {
1243
+ await this.#clearAuthority(tenant);
1034
1244
  return false;
1035
1245
  }
1246
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
1247
+ if (probe?.ok) {
1248
+ this.#activeSessions.set(tenant.stateRoot, { cids: fingerprint, until: Date.now() + 3e4 });
1249
+ }
1250
+ return true;
1251
+ }
1252
+ async #clearAuthority(tenant) {
1253
+ const current = this.#clearing.get(tenant.stateRoot);
1254
+ if (current) return current;
1255
+ this.#activeSessions.delete(tenant.stateRoot);
1256
+ this.#renewalRequired.add(tenant.stateRoot);
1257
+ this.#clearedScoped.delete(tenant.stateRoot);
1258
+ this.#approvalEpochs.set(
1259
+ tenant.stateRoot,
1260
+ (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1
1261
+ );
1262
+ const clearing = (async () => {
1263
+ const processing = this.#approvalProcessing.get(tenant.stateRoot);
1264
+ if (processing) await processing.catch(() => void 0);
1265
+ const scoped = this.#scopedClearing.get(tenant.stateRoot);
1266
+ if (scoped) await scoped;
1267
+ this.#renewalRequired.add(tenant.stateRoot);
1268
+ const profilePath = this.#profilePath(tenant.stateRoot);
1269
+ const profile = await readJson(profilePath);
1270
+ await secureWrite(profilePath, {
1271
+ ...profile,
1272
+ approvalEpoch: this.#approvalEpochs.get(tenant.stateRoot)
1273
+ });
1274
+ await rm(this.#additionalPath(tenant.stateRoot), { force: true });
1275
+ await rm(join2(tenant.stateRoot, ".tinycloud", "profiles", PROFILE, "auth-requests.json"), { force: true });
1276
+ await rm(this.#pendingDirectory(tenant.stateRoot), { recursive: true, force: true });
1277
+ await rm(this.#sessionPath(tenant.stateRoot), { force: true });
1278
+ })().finally(() => this.#clearing.delete(tenant.stateRoot));
1279
+ this.#clearing.set(tenant.stateRoot, clearing);
1280
+ await clearing;
1036
1281
  }
1037
1282
  async #initializeTenant(tenantId) {
1038
1283
  const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
@@ -1071,21 +1316,36 @@ var RemoteTenantStore = class {
1071
1316
  `
1072
1317
  );
1073
1318
  }
1074
- return { stateRoot, sessionDid, jwk };
1319
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
1320
+ this.#approvalEpochs.set(stateRoot, approvalEpoch);
1321
+ return { stateRoot, sessionDid, jwk, approvalEpoch };
1075
1322
  }
1076
1323
  async #readTenant(tenantId) {
1077
1324
  if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1078
1325
  const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
1079
1326
  const jwk = await readJson(this.#keyPath(stateRoot));
1080
1327
  if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
1081
- return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
1328
+ const profile = await readJson(this.#profilePath(stateRoot));
1329
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
1330
+ return { stateRoot, jwk, sessionDid: didFromJwk(jwk), approvalEpoch };
1082
1331
  }
1083
1332
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
1333
+ const clearing = this.#clearing.get(this.tenantStateRoot(subject));
1334
+ if (clearing) await clearing;
1084
1335
  const tenant = await this.#ensureTenant(subject);
1085
1336
  const tenantId = this.#tenantId(subject);
1337
+ const normalizedOwners = ownerDids.map(normalizeDid);
1338
+ const key = JSON.stringify([tenantId, tenant.approvalEpoch, kind, requestId, permissions, normalizedOwners]);
1339
+ const current = this.#pendingWrites.get(key);
1340
+ if (current) return current;
1341
+ const writing = this.#writePending(tenant, tenantId, kind, permissions, normalizedOwners, requestId).finally(() => this.#pendingWrites.delete(key));
1342
+ this.#pendingWrites.set(key, writing);
1343
+ return writing;
1344
+ }
1345
+ async #writePending(tenant, tenantId, kind, permissions, ownerDids, requestId) {
1086
1346
  const reusable = await reusablePending(
1087
1347
  this.#pendingDirectory(tenant.stateRoot),
1088
- { tenantId, kind, requestId, permissions, ownerDids: ownerDids.map(normalizeDid) }
1348
+ { tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
1089
1349
  );
1090
1350
  if (reusable !== void 0) return reusable;
1091
1351
  const nonce = randomBytes(24).toString("base64url");
@@ -1094,10 +1354,11 @@ var RemoteTenantStore = class {
1094
1354
  version: 1,
1095
1355
  nonce,
1096
1356
  tenantId,
1357
+ approvalEpoch: tenant.approvalEpoch,
1097
1358
  kind,
1098
1359
  ...requestId === void 0 ? {} : { requestId },
1099
1360
  permissions,
1100
- ownerDids: ownerDids.map(normalizeDid),
1361
+ ownerDids,
1101
1362
  createdAt: new Date(created).toISOString(),
1102
1363
  expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
1103
1364
  };
@@ -1138,6 +1399,11 @@ var RemoteTenantStore = class {
1138
1399
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
1139
1400
  throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
1140
1401
  }
1402
+ const profile = await readJson(this.#profilePath(stateRoot));
1403
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
1404
+ if ((pending.approvalEpoch ?? 0) !== approvalEpoch) {
1405
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
1406
+ }
1141
1407
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
1142
1408
  await rm(consume ? claimed : path, { force: true });
1143
1409
  throw new ApprovalRequestError("state_expired", "Approval state has expired.");
@@ -1173,6 +1439,9 @@ var RemoteTenantStore = class {
1173
1439
  #sessionPath(stateRoot) {
1174
1440
  return join2(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
1175
1441
  }
1442
+ #additionalPath(stateRoot) {
1443
+ return join2(stateRoot, ".tinycloud", "profiles", PROFILE, "additional-delegations.json");
1444
+ }
1176
1445
  #pendingPath(stateRoot, nonce) {
1177
1446
  return join2(this.#pendingDirectory(stateRoot), `${nonce}.json`);
1178
1447
  }
@@ -1200,6 +1469,21 @@ function ensureWasm() {
1200
1469
  initPanicHook();
1201
1470
  wasmInitialized = true;
1202
1471
  }
1472
+ function revocationFromResult(error) {
1473
+ const details = error.details;
1474
+ if (!isRecord(details) || typeof details.revokedCid !== "string") return void 0;
1475
+ if (details.reason === "delegation_revoked") {
1476
+ return { reason: "delegation_revoked", revokedCid: details.revokedCid };
1477
+ }
1478
+ if (details.reason === "delegation_ancestor_revoked" && typeof details.ancestorCid === "string") {
1479
+ return {
1480
+ reason: "delegation_ancestor_revoked",
1481
+ revokedCid: details.revokedCid,
1482
+ ancestorCid: details.ancestorCid
1483
+ };
1484
+ }
1485
+ return void 0;
1486
+ }
1203
1487
  function sessionFromCallback(data, privateJwk) {
1204
1488
  const delegationHeader = data.delegationHeader;
1205
1489
  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") {
@@ -1290,7 +1574,7 @@ async function reusablePending(directory, expected) {
1290
1574
  return void 0;
1291
1575
  }
1292
1576
  function samePendingRequest(pending, expected) {
1293
- 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);
1577
+ 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);
1294
1578
  }
1295
1579
 
1296
1580
  // src/http.ts
@@ -1330,7 +1614,8 @@ function createHostedMcpApp(config) {
1330
1614
  explicitProfile: true,
1331
1615
  allowOwnerProfile: false,
1332
1616
  stateRoot,
1333
- transformOperationResult: (result) => store.decorateAuthorityResult(subject, ownerDids, result)
1617
+ beforeOperation: () => store.validateTenant(subject, ownerDids),
1618
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1334
1619
  }, validator);
1335
1620
  registerConnectTool(server, validator, store, identity);
1336
1621
  return server;