@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/http.js CHANGED
@@ -112,10 +112,12 @@ import {
112
112
  UnsupportedSessionRestoreError,
113
113
  isCapabilitySubset,
114
114
  principalDidEquals,
115
- siweRuntimeDelegation
115
+ siweRuntimeDelegation,
116
+ signedCompactUcanExpiry
116
117
  } from "@tinycloud/node-sdk";
117
118
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
118
119
  import { invokeOperation } from "@tinycloud/operations";
120
+ import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
119
121
  import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
120
122
  var PROFILE = "agent";
121
123
  var MAX_CALLBACK_BYTES = 256 * 1024;
@@ -163,6 +165,15 @@ var ApprovalRequestError = class extends Error {
163
165
  var RemoteTenantStore = class {
164
166
  #config;
165
167
  #initializing = /* @__PURE__ */ new Map();
168
+ #sessionChecks = /* @__PURE__ */ new Map();
169
+ #clearing = /* @__PURE__ */ new Map();
170
+ #activeSessions = /* @__PURE__ */ new Map();
171
+ #pendingWrites = /* @__PURE__ */ new Map();
172
+ #renewalRequired = /* @__PURE__ */ new Set();
173
+ #approvalEpochs = /* @__PURE__ */ new Map();
174
+ #scopedClearing = /* @__PURE__ */ new Map();
175
+ #clearedScoped = /* @__PURE__ */ new Map();
176
+ #approvalProcessing = /* @__PURE__ */ new Map();
166
177
  constructor(config) {
167
178
  this.#config = config;
168
179
  }
@@ -173,9 +184,13 @@ var RemoteTenantStore = class {
173
184
  async initializeTenant(subject) {
174
185
  return (await this.#ensureTenant(subject)).stateRoot;
175
186
  }
187
+ /** Check stored node authority before an authenticated operation uses it. */
188
+ async validateTenant(subject, ownerDids) {
189
+ await this.#hasValidSession(await this.#ensureTenant(subject), ownerDids);
190
+ }
176
191
  async connectStatus(subject, ownerDids = []) {
177
192
  const tenant = await this.#ensureTenant(subject);
178
- if (await this.#hasValidSession(tenant, ownerDids)) {
193
+ if (await this.#hasValidSession(tenant, ownerDids, true)) {
179
194
  return { connected: true, sessionDid: tenant.sessionDid };
180
195
  }
181
196
  const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
@@ -186,24 +201,54 @@ var RemoteTenantStore = class {
186
201
  expiresAt: pending.expiresAt
187
202
  };
188
203
  }
189
- async decorateAuthorityResult(subject, ownerDids, result) {
204
+ async decorateAuthorityResult(subject, ownerDids, result, invocation) {
190
205
  if (!isRecord(result)) return result;
191
206
  if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
192
- await this.#ensureTenant(subject);
207
+ const tenant2 = await this.#ensureTenant(subject);
208
+ const refusal = revocationFromResult(result.error);
209
+ const cleared = refusal === void 0 ? void 0 : await this.#clearRefusedAuthority(tenant2, refusal);
210
+ if (cleared === "scoped" && invocation !== void 0) {
211
+ return this.#renewScopedApproval(subject, ownerDids, tenant2, result, invocation);
212
+ }
213
+ const valid = await this.#hasValidSession(tenant2, ownerDids, true);
214
+ if (valid && result.error.code === "AUTH_REQUIRED") {
215
+ if (refusal === void 0) return result;
216
+ return {
217
+ ...result,
218
+ error: { code: "AUTH_REQUIRED", message: "TinyCloud approval is required.", retryable: false }
219
+ };
220
+ }
193
221
  const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
194
222
  return {
195
- ...result,
223
+ status: "error",
224
+ operation: result.operation,
225
+ context: result.context,
196
226
  error: {
197
- ...result.error,
198
- details: {
199
- ...isRecord(result.error.details) ? result.error.details : {},
200
- approvalUrl: this.#connectUrl(pending2),
201
- expiresAt: pending2.expiresAt
202
- }
227
+ code: result.error.code === "SESSION_NOT_FOUND" && await readJson(this.#sessionPath(tenant2.stateRoot)) === null ? "SESSION_NOT_FOUND" : "AUTH_REQUIRED",
228
+ message: "A new TinyCloud approval is required.",
229
+ retryable: false,
230
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
203
231
  }
204
232
  };
205
233
  }
206
234
  if (result.status !== "authority_required") return result;
235
+ const tenant = await this.#ensureTenant(subject);
236
+ if (this.#renewalRequired.has(tenant.stateRoot) || !await this.#hasValidSession(tenant, ownerDids, true)) {
237
+ if (this.#renewalRequired.has(tenant.stateRoot)) {
238
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
239
+ return {
240
+ status: "error",
241
+ operation: result.operation,
242
+ context: result.context,
243
+ error: {
244
+ code: "AUTH_REQUIRED",
245
+ message: "A new TinyCloud approval is required.",
246
+ retryable: false,
247
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
248
+ }
249
+ };
250
+ }
251
+ }
207
252
  const request = isRecord(result.request) ? result.request : void 0;
208
253
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
209
254
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -220,6 +265,90 @@ var RemoteTenantStore = class {
220
265
  }
221
266
  };
222
267
  }
268
+ async #renewScopedApproval(subject, ownerDids, tenant, result, invocation) {
269
+ const planned = await invokeOperation(
270
+ "tinycloud.auth.request",
271
+ 1,
272
+ { profile: PROFILE, stateRoot: tenant.stateRoot },
273
+ invocation
274
+ );
275
+ const output = planned.status === "ok" && isRecord(planned.output) ? planned.output : void 0;
276
+ const request = isRecord(output?.request) ? output.request : void 0;
277
+ const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
278
+ const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
279
+ const missing = Array.isArray(output?.missing) ? output.missing.filter(isPermissionEntry) : [];
280
+ if (requestId === void 0 || requested.length === 0 || missing.length === 0) {
281
+ return {
282
+ status: "error",
283
+ operation: result.operation,
284
+ context: result.context,
285
+ error: { code: "AUTH_REQUIRED", message: "A new TinyCloud approval is required.", retryable: false }
286
+ };
287
+ }
288
+ const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
289
+ return {
290
+ status: "authority_required",
291
+ operation: result.operation,
292
+ context: result.context,
293
+ missing,
294
+ request,
295
+ approval: {
296
+ kind: "openkey",
297
+ requestId,
298
+ url: this.#connectUrl(pending),
299
+ fallback: tcAppsApprovalMessage(requested) ?? "Open the approval URL, then retry the same MCP tool."
300
+ },
301
+ retry: operationRetryDescriptor(invocation, invocation.input)
302
+ };
303
+ }
304
+ async #clearRefusedAuthority(tenant, refusal) {
305
+ const raw = await readJson(this.#sessionPath(tenant.stateRoot)).catch(() => null);
306
+ const bootstrapCid = raw?.delegationCid;
307
+ const ancestorCid = refusal.reason === "delegation_ancestor_revoked" ? refusal.ancestorCid : void 0;
308
+ if (typeof bootstrapCid === "string" && (bootstrapCid === refusal.revokedCid || bootstrapCid === ancestorCid)) {
309
+ await this.#clearAuthority(tenant);
310
+ return "bootstrap";
311
+ }
312
+ const key = tenant.stateRoot;
313
+ const removed = this.#clearedScoped.get(tenant.stateRoot);
314
+ if (removed?.has(refusal.revokedCid)) return "scoped";
315
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
316
+ if (this.#clearedScoped.get(key)?.has(refusal.revokedCid)) return "scoped";
317
+ if (!Array.isArray(additional)) return void 0;
318
+ const affected = additional.some((entry) => {
319
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
320
+ return typeof cid === "string" && (cid === refusal.revokedCid || cid === ancestorCid);
321
+ });
322
+ const inProgress = this.#scopedClearing.get(key);
323
+ if (inProgress) {
324
+ await inProgress;
325
+ return this.#clearRefusedAuthority(tenant, refusal);
326
+ }
327
+ if (!affected) return void 0;
328
+ const clearing = (async () => {
329
+ const latest = await readJson(this.#additionalPath(tenant.stateRoot));
330
+ if (!Array.isArray(latest)) return;
331
+ const remaining = latest.filter((entry) => {
332
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
333
+ return typeof cid !== "string" || cid !== refusal.revokedCid && cid !== ancestorCid;
334
+ });
335
+ if (remaining.length === latest.length) return;
336
+ this.#activeSessions.delete(tenant.stateRoot);
337
+ const epoch = (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1;
338
+ this.#approvalEpochs.set(tenant.stateRoot, epoch);
339
+ const profilePath = this.#profilePath(tenant.stateRoot);
340
+ const profile = await readJson(profilePath);
341
+ await secureWrite(profilePath, { ...profile, approvalEpoch: epoch });
342
+ if (remaining.length === 0) await rm(this.#additionalPath(tenant.stateRoot), { force: true });
343
+ else await secureWrite(this.#additionalPath(tenant.stateRoot), remaining);
344
+ const cleared = removed ?? /* @__PURE__ */ new Set();
345
+ cleared.add(refusal.revokedCid);
346
+ this.#clearedScoped.set(tenant.stateRoot, cleared);
347
+ })().finally(() => this.#scopedClearing.delete(key));
348
+ this.#scopedClearing.set(key, clearing);
349
+ await clearing;
350
+ return "scoped";
351
+ }
223
352
  async approvalRedirect(state) {
224
353
  const pending = await this.#readPending(state, false);
225
354
  const tenant = await this.#readTenant(pending.tenantId);
@@ -260,21 +389,36 @@ var RemoteTenantStore = class {
260
389
  }
261
390
  if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
262
391
  const pending = await this.#readPending(state, true);
263
- try {
264
- const tenant = await this.#readTenant(pending.tenantId);
265
- if (pending.kind === "bootstrap") {
266
- await this.#storeBootstrap(tenant, pending, data);
267
- } else {
268
- await this.#storeDelegation(tenant, pending, data);
392
+ const stateRoot = join(this.#config.stateDir, "tenants", pending.tenantId);
393
+ const prior = this.#approvalProcessing.get(stateRoot);
394
+ const processing = (async () => {
395
+ if (prior) await prior.catch(() => void 0);
396
+ try {
397
+ const tenant = await this.#readTenant(pending.tenantId);
398
+ if ((pending.approvalEpoch ?? 0) !== tenant.approvalEpoch) {
399
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
400
+ }
401
+ if (pending.kind === "bootstrap") {
402
+ await this.#storeBootstrap(tenant, pending, data);
403
+ } else {
404
+ await this.#storeDelegation(tenant, pending, data);
405
+ }
406
+ await rm(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
407
+ } catch (error) {
408
+ await this.#restorePending(pending).catch(() => void 0);
409
+ throw error;
269
410
  }
270
- await rm(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
271
- } catch (error) {
272
- await this.#restorePending(pending).catch(() => void 0);
273
- throw error;
411
+ })();
412
+ this.#approvalProcessing.set(stateRoot, processing);
413
+ try {
414
+ await processing;
415
+ } finally {
416
+ if (this.#approvalProcessing.get(stateRoot) === processing) this.#approvalProcessing.delete(stateRoot);
274
417
  }
275
418
  }
276
419
  async #storeBootstrap(tenant, pending, data) {
277
420
  const { node, session } = await this.#restoreApproval(tenant, data);
421
+ this.#assertCurrentApproval(pending, session);
278
422
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
279
423
  throw new ApprovalOwnerMismatchError();
280
424
  }
@@ -291,10 +435,13 @@ var RemoteTenantStore = class {
291
435
  authMethod: "openkey",
292
436
  ...session
293
437
  });
438
+ this.#renewalRequired.delete(tenant.stateRoot);
439
+ this.#activeSessions.delete(tenant.stateRoot);
294
440
  }
295
441
  async #storeDelegation(tenant, pending, data) {
296
442
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
297
443
  const { node, session } = await this.#restoreApproval(tenant, data);
444
+ this.#assertCurrentApproval(pending, session);
298
445
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
299
446
  let delegation;
300
447
  try {
@@ -338,6 +485,12 @@ var RemoteTenantStore = class {
338
485
  }
339
486
  if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
340
487
  }
488
+ #assertCurrentApproval(pending, session) {
489
+ const issuedAt = /^Issued At: ([^\r\n]+)\r?$/m.exec(session.siwe)?.[1];
490
+ if (issuedAt === void 0 || !Number.isFinite(Date.parse(issuedAt)) || Date.parse(issuedAt) < Date.parse(pending.createdAt)) {
491
+ throw new ApprovalStageError("restore_approval", "APPROVAL_PREDATES_REQUEST");
492
+ }
493
+ }
341
494
  /**
342
495
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
343
496
  * callback must carry the full signed proof (SIWE, signature, address and
@@ -378,17 +531,104 @@ var RemoteTenantStore = class {
378
531
  this.#initializing.set(tenantId, initializing);
379
532
  return initializing;
380
533
  }
381
- async #hasValidSession(tenant, ownerDids) {
382
- const raw = await readJson(this.#sessionPath(tenant.stateRoot));
534
+ async #hasValidSession(tenant, ownerDids, fresh = false) {
535
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
536
+ const key = JSON.stringify([tenant.stateRoot, ownerDids.map(normalizeDid), fresh]);
537
+ const current = this.#sessionChecks.get(key);
538
+ if (current) return current;
539
+ const checking = this.#checkSession(tenant, ownerDids, fresh).finally(() => {
540
+ this.#sessionChecks.delete(key);
541
+ });
542
+ this.#sessionChecks.set(key, checking);
543
+ return checking;
544
+ }
545
+ async #checkSession(tenant, ownerDids, fresh) {
546
+ let raw;
547
+ try {
548
+ raw = await readJson(this.#sessionPath(tenant.stateRoot));
549
+ } catch (error) {
550
+ if (!(error instanceof SyntaxError)) throw error;
551
+ await this.#clearAuthority(tenant);
552
+ return false;
553
+ }
383
554
  if (raw === null) return false;
555
+ const node = new TinyCloudNode({ host: this.#config.nodeHost });
384
556
  try {
385
- const node = new TinyCloudNode({ host: this.#config.nodeHost });
386
557
  await node.restoreSession(raw);
387
- return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
388
558
  } catch {
389
- await rm(this.#sessionPath(tenant.stateRoot), { force: true });
559
+ const header = isRecord(raw.delegationHeader) ? raw.delegationHeader.Authorization : void 0;
560
+ const signedExpiry = typeof header === "string" ? signedCompactUcanExpiry(header) : void 0;
561
+ const siweExpiry = typeof raw.siwe === "string" ? /^Expiration Time: ([^\r\n]+)\r?$/m.exec(raw.siwe)?.[1] : void 0;
562
+ const expiry = signedExpiry ?? (siweExpiry === void 0 ? void 0 : new Date(siweExpiry));
563
+ if (expiry !== void 0 && expiry.getTime() <= Date.now()) return false;
564
+ await this.#clearAuthority(tenant);
565
+ return false;
566
+ }
567
+ if (!samePrincipal(node.sessionDid, tenant.sessionDid) || !ownerDids.some((did) => samePrincipal(node.did, did))) return false;
568
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
569
+ const cids = [raw.delegationCid];
570
+ if (additional !== null) {
571
+ if (!Array.isArray(additional)) throw new Error("Stored TinyCloud authority is unavailable.");
572
+ for (const entry of additional) {
573
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
574
+ cids.push(cid);
575
+ }
576
+ }
577
+ if (!cids.every((cid) => typeof cid === "string" && cid.length > 0)) {
578
+ throw new Error("Stored TinyCloud authority is unavailable.");
579
+ }
580
+ const fingerprint = cids.join("\n");
581
+ const cached = this.#activeSessions.get(tenant.stateRoot);
582
+ if (!fresh && cached?.cids === fingerprint && cached.until > Date.now()) return true;
583
+ let timer;
584
+ const timeout = new Promise((resolve) => {
585
+ timer = setTimeout(() => resolve(void 0), 2e3);
586
+ timer.unref();
587
+ });
588
+ const probe = await Promise.race([
589
+ node.kvForSpace(String(raw.spaceId)).list({ prefix: "spaces/" }).catch(() => void 0),
590
+ timeout
591
+ ]).finally(() => clearTimeout(timer));
592
+ const refusal = probe !== void 0 && !probe.ok ? nodeRevocationOf(probe.error) : void 0;
593
+ if (refusal !== void 0 && (refusal.revokedCid === raw.delegationCid || refusal.reason === "delegation_ancestor_revoked" && refusal.ancestorCid === raw.delegationCid)) {
594
+ await this.#clearAuthority(tenant);
390
595
  return false;
391
596
  }
597
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
598
+ if (probe?.ok) {
599
+ this.#activeSessions.set(tenant.stateRoot, { cids: fingerprint, until: Date.now() + 3e4 });
600
+ }
601
+ return true;
602
+ }
603
+ async #clearAuthority(tenant) {
604
+ const current = this.#clearing.get(tenant.stateRoot);
605
+ if (current) return current;
606
+ this.#activeSessions.delete(tenant.stateRoot);
607
+ this.#renewalRequired.add(tenant.stateRoot);
608
+ this.#clearedScoped.delete(tenant.stateRoot);
609
+ this.#approvalEpochs.set(
610
+ tenant.stateRoot,
611
+ (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1
612
+ );
613
+ const clearing = (async () => {
614
+ const processing = this.#approvalProcessing.get(tenant.stateRoot);
615
+ if (processing) await processing.catch(() => void 0);
616
+ const scoped = this.#scopedClearing.get(tenant.stateRoot);
617
+ if (scoped) await scoped;
618
+ this.#renewalRequired.add(tenant.stateRoot);
619
+ const profilePath = this.#profilePath(tenant.stateRoot);
620
+ const profile = await readJson(profilePath);
621
+ await secureWrite(profilePath, {
622
+ ...profile,
623
+ approvalEpoch: this.#approvalEpochs.get(tenant.stateRoot)
624
+ });
625
+ await rm(this.#additionalPath(tenant.stateRoot), { force: true });
626
+ await rm(join(tenant.stateRoot, ".tinycloud", "profiles", PROFILE, "auth-requests.json"), { force: true });
627
+ await rm(this.#pendingDirectory(tenant.stateRoot), { recursive: true, force: true });
628
+ await rm(this.#sessionPath(tenant.stateRoot), { force: true });
629
+ })().finally(() => this.#clearing.delete(tenant.stateRoot));
630
+ this.#clearing.set(tenant.stateRoot, clearing);
631
+ await clearing;
392
632
  }
393
633
  async #initializeTenant(tenantId) {
394
634
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
@@ -427,21 +667,36 @@ var RemoteTenantStore = class {
427
667
  `
428
668
  );
429
669
  }
430
- return { stateRoot, sessionDid, jwk };
670
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
671
+ this.#approvalEpochs.set(stateRoot, approvalEpoch);
672
+ return { stateRoot, sessionDid, jwk, approvalEpoch };
431
673
  }
432
674
  async #readTenant(tenantId) {
433
675
  if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
434
676
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
435
677
  const jwk = await readJson(this.#keyPath(stateRoot));
436
678
  if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
437
- return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
679
+ const profile = await readJson(this.#profilePath(stateRoot));
680
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
681
+ return { stateRoot, jwk, sessionDid: didFromJwk(jwk), approvalEpoch };
438
682
  }
439
683
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
684
+ const clearing = this.#clearing.get(this.tenantStateRoot(subject));
685
+ if (clearing) await clearing;
440
686
  const tenant = await this.#ensureTenant(subject);
441
687
  const tenantId = this.#tenantId(subject);
688
+ const normalizedOwners = ownerDids.map(normalizeDid);
689
+ const key = JSON.stringify([tenantId, tenant.approvalEpoch, kind, requestId, permissions, normalizedOwners]);
690
+ const current = this.#pendingWrites.get(key);
691
+ if (current) return current;
692
+ const writing = this.#writePending(tenant, tenantId, kind, permissions, normalizedOwners, requestId).finally(() => this.#pendingWrites.delete(key));
693
+ this.#pendingWrites.set(key, writing);
694
+ return writing;
695
+ }
696
+ async #writePending(tenant, tenantId, kind, permissions, ownerDids, requestId) {
442
697
  const reusable = await reusablePending(
443
698
  this.#pendingDirectory(tenant.stateRoot),
444
- { tenantId, kind, requestId, permissions, ownerDids: ownerDids.map(normalizeDid) }
699
+ { tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
445
700
  );
446
701
  if (reusable !== void 0) return reusable;
447
702
  const nonce = randomBytes(24).toString("base64url");
@@ -450,10 +705,11 @@ var RemoteTenantStore = class {
450
705
  version: 1,
451
706
  nonce,
452
707
  tenantId,
708
+ approvalEpoch: tenant.approvalEpoch,
453
709
  kind,
454
710
  ...requestId === void 0 ? {} : { requestId },
455
711
  permissions,
456
- ownerDids: ownerDids.map(normalizeDid),
712
+ ownerDids,
457
713
  createdAt: new Date(created).toISOString(),
458
714
  expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
459
715
  };
@@ -494,6 +750,11 @@ var RemoteTenantStore = class {
494
750
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
495
751
  throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
496
752
  }
753
+ const profile = await readJson(this.#profilePath(stateRoot));
754
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
755
+ if ((pending.approvalEpoch ?? 0) !== approvalEpoch) {
756
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
757
+ }
497
758
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
498
759
  await rm(consume ? claimed : path, { force: true });
499
760
  throw new ApprovalRequestError("state_expired", "Approval state has expired.");
@@ -529,6 +790,9 @@ var RemoteTenantStore = class {
529
790
  #sessionPath(stateRoot) {
530
791
  return join(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
531
792
  }
793
+ #additionalPath(stateRoot) {
794
+ return join(stateRoot, ".tinycloud", "profiles", PROFILE, "additional-delegations.json");
795
+ }
532
796
  #pendingPath(stateRoot, nonce) {
533
797
  return join(this.#pendingDirectory(stateRoot), `${nonce}.json`);
534
798
  }
@@ -556,6 +820,21 @@ function ensureWasm() {
556
820
  initPanicHook();
557
821
  wasmInitialized = true;
558
822
  }
823
+ function revocationFromResult(error) {
824
+ const details = error.details;
825
+ if (!isRecord(details) || typeof details.revokedCid !== "string") return void 0;
826
+ if (details.reason === "delegation_revoked") {
827
+ return { reason: "delegation_revoked", revokedCid: details.revokedCid };
828
+ }
829
+ if (details.reason === "delegation_ancestor_revoked" && typeof details.ancestorCid === "string") {
830
+ return {
831
+ reason: "delegation_ancestor_revoked",
832
+ revokedCid: details.revokedCid,
833
+ ancestorCid: details.ancestorCid
834
+ };
835
+ }
836
+ return void 0;
837
+ }
559
838
  function sessionFromCallback(data, privateJwk) {
560
839
  const delegationHeader = data.delegationHeader;
561
840
  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") {
@@ -646,7 +925,7 @@ async function reusablePending(directory, expected) {
646
925
  return void 0;
647
926
  }
648
927
  function samePendingRequest(pending, expected) {
649
- 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);
928
+ 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);
650
929
  }
651
930
 
652
931
  // src/server.ts
@@ -1011,6 +1290,7 @@ function registerTinyCloudTools(server, selection, validator) {
1011
1290
  const metrics = { replayedGrants: 0 };
1012
1291
  let code = "INTERNAL_ERROR";
1013
1292
  try {
1293
+ await selection.beforeOperation?.();
1014
1294
  const result = await invokeOperation2(
1015
1295
  binding.operationId,
1016
1296
  binding.operationVersion,
@@ -1018,7 +1298,11 @@ function registerTinyCloudTools(server, selection, validator) {
1018
1298
  input,
1019
1299
  metrics
1020
1300
  );
1021
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1301
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result, {
1302
+ operationId: binding.operationId,
1303
+ operationVersion: binding.operationVersion,
1304
+ input
1305
+ });
1022
1306
  const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1023
1307
  surface: selection.surface,
1024
1308
  readOnly: binding.readOnlyHint,
@@ -1050,7 +1334,7 @@ function outcomeCode(result) {
1050
1334
  // package.json
1051
1335
  var package_default = {
1052
1336
  name: "@tinycloud/mcp",
1053
- version: "0.7.0-beta.35",
1337
+ version: "0.7.0-beta.36",
1054
1338
  description: "TinyCloud delegated operations MCP server",
1055
1339
  keywords: [
1056
1340
  "tinycloud",
@@ -1123,7 +1407,7 @@ var package_default = {
1123
1407
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1124
1408
  "@tinycloud/node-sdk": "3.3.0-beta.36",
1125
1409
  "@tinycloud/node-sdk-wasm": "1.7.8-beta.0",
1126
- "@tinycloud/operations": "0.7.0-beta.34",
1410
+ "@tinycloud/operations": "0.7.0-beta.35",
1127
1411
  jose: "6.2.4"
1128
1412
  },
1129
1413
  devDependencies: {
@@ -1182,7 +1466,8 @@ function createHostedMcpApp(config) {
1182
1466
  explicitProfile: true,
1183
1467
  allowOwnerProfile: false,
1184
1468
  stateRoot,
1185
- transformOperationResult: (result) => store.decorateAuthorityResult(subject, ownerDids, result)
1469
+ beforeOperation: () => store.validateTenant(subject, ownerDids),
1470
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1186
1471
  }, validator);
1187
1472
  registerConnectTool(server, validator, store, identity);
1188
1473
  return server;