@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-cli.cjs CHANGED
@@ -138,6 +138,7 @@ var import_node_path = require("path");
138
138
  var import_node_sdk2 = require("@tinycloud/node-sdk");
139
139
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
140
140
  var import_operations = require("@tinycloud/operations");
141
+ var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
141
142
  var import_tc_apps = require("@tinycloud/operations/tc-apps");
142
143
  var PROFILE = "agent";
143
144
  var MAX_CALLBACK_BYTES = 256 * 1024;
@@ -185,6 +186,15 @@ var ApprovalRequestError = class extends Error {
185
186
  var RemoteTenantStore = class {
186
187
  #config;
187
188
  #initializing = /* @__PURE__ */ new Map();
189
+ #sessionChecks = /* @__PURE__ */ new Map();
190
+ #clearing = /* @__PURE__ */ new Map();
191
+ #activeSessions = /* @__PURE__ */ new Map();
192
+ #pendingWrites = /* @__PURE__ */ new Map();
193
+ #renewalRequired = /* @__PURE__ */ new Set();
194
+ #approvalEpochs = /* @__PURE__ */ new Map();
195
+ #scopedClearing = /* @__PURE__ */ new Map();
196
+ #clearedScoped = /* @__PURE__ */ new Map();
197
+ #approvalProcessing = /* @__PURE__ */ new Map();
188
198
  constructor(config) {
189
199
  this.#config = config;
190
200
  }
@@ -195,9 +205,13 @@ var RemoteTenantStore = class {
195
205
  async initializeTenant(subject) {
196
206
  return (await this.#ensureTenant(subject)).stateRoot;
197
207
  }
208
+ /** Check stored node authority before an authenticated operation uses it. */
209
+ async validateTenant(subject, ownerDids) {
210
+ await this.#hasValidSession(await this.#ensureTenant(subject), ownerDids);
211
+ }
198
212
  async connectStatus(subject, ownerDids = []) {
199
213
  const tenant = await this.#ensureTenant(subject);
200
- if (await this.#hasValidSession(tenant, ownerDids)) {
214
+ if (await this.#hasValidSession(tenant, ownerDids, true)) {
201
215
  return { connected: true, sessionDid: tenant.sessionDid };
202
216
  }
203
217
  const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
@@ -208,24 +222,54 @@ var RemoteTenantStore = class {
208
222
  expiresAt: pending.expiresAt
209
223
  };
210
224
  }
211
- async decorateAuthorityResult(subject, ownerDids, result) {
225
+ async decorateAuthorityResult(subject, ownerDids, result, invocation) {
212
226
  if (!isRecord(result)) return result;
213
227
  if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
214
- await this.#ensureTenant(subject);
228
+ const tenant2 = await this.#ensureTenant(subject);
229
+ const refusal = revocationFromResult(result.error);
230
+ const cleared = refusal === void 0 ? void 0 : await this.#clearRefusedAuthority(tenant2, refusal);
231
+ if (cleared === "scoped" && invocation !== void 0) {
232
+ return this.#renewScopedApproval(subject, ownerDids, tenant2, result, invocation);
233
+ }
234
+ const valid = await this.#hasValidSession(tenant2, ownerDids, true);
235
+ if (valid && result.error.code === "AUTH_REQUIRED") {
236
+ if (refusal === void 0) return result;
237
+ return {
238
+ ...result,
239
+ error: { code: "AUTH_REQUIRED", message: "TinyCloud approval is required.", retryable: false }
240
+ };
241
+ }
215
242
  const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
216
243
  return {
217
- ...result,
244
+ status: "error",
245
+ operation: result.operation,
246
+ context: result.context,
218
247
  error: {
219
- ...result.error,
220
- details: {
221
- ...isRecord(result.error.details) ? result.error.details : {},
222
- approvalUrl: this.#connectUrl(pending2),
223
- expiresAt: pending2.expiresAt
224
- }
248
+ code: result.error.code === "SESSION_NOT_FOUND" && await readJson(this.#sessionPath(tenant2.stateRoot)) === null ? "SESSION_NOT_FOUND" : "AUTH_REQUIRED",
249
+ message: "A new TinyCloud approval is required.",
250
+ retryable: false,
251
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
225
252
  }
226
253
  };
227
254
  }
228
255
  if (result.status !== "authority_required") return result;
256
+ const tenant = await this.#ensureTenant(subject);
257
+ if (this.#renewalRequired.has(tenant.stateRoot) || !await this.#hasValidSession(tenant, ownerDids, true)) {
258
+ if (this.#renewalRequired.has(tenant.stateRoot)) {
259
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
260
+ return {
261
+ status: "error",
262
+ operation: result.operation,
263
+ context: result.context,
264
+ error: {
265
+ code: "AUTH_REQUIRED",
266
+ message: "A new TinyCloud approval is required.",
267
+ retryable: false,
268
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
269
+ }
270
+ };
271
+ }
272
+ }
229
273
  const request = isRecord(result.request) ? result.request : void 0;
230
274
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
231
275
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -242,6 +286,90 @@ var RemoteTenantStore = class {
242
286
  }
243
287
  };
244
288
  }
289
+ async #renewScopedApproval(subject, ownerDids, tenant, result, invocation) {
290
+ const planned = await (0, import_operations.invokeOperation)(
291
+ "tinycloud.auth.request",
292
+ 1,
293
+ { profile: PROFILE, stateRoot: tenant.stateRoot },
294
+ invocation
295
+ );
296
+ const output = planned.status === "ok" && isRecord(planned.output) ? planned.output : void 0;
297
+ const request = isRecord(output?.request) ? output.request : void 0;
298
+ const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
299
+ const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
300
+ const missing = Array.isArray(output?.missing) ? output.missing.filter(isPermissionEntry) : [];
301
+ if (requestId === void 0 || requested.length === 0 || missing.length === 0) {
302
+ return {
303
+ status: "error",
304
+ operation: result.operation,
305
+ context: result.context,
306
+ error: { code: "AUTH_REQUIRED", message: "A new TinyCloud approval is required.", retryable: false }
307
+ };
308
+ }
309
+ const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
310
+ return {
311
+ status: "authority_required",
312
+ operation: result.operation,
313
+ context: result.context,
314
+ missing,
315
+ request,
316
+ approval: {
317
+ kind: "openkey",
318
+ requestId,
319
+ url: this.#connectUrl(pending),
320
+ fallback: (0, import_tc_apps.tcAppsApprovalMessage)(requested) ?? "Open the approval URL, then retry the same MCP tool."
321
+ },
322
+ retry: (0, import_mcp_authority.operationRetryDescriptor)(invocation, invocation.input)
323
+ };
324
+ }
325
+ async #clearRefusedAuthority(tenant, refusal) {
326
+ const raw = await readJson(this.#sessionPath(tenant.stateRoot)).catch(() => null);
327
+ const bootstrapCid = raw?.delegationCid;
328
+ const ancestorCid = refusal.reason === "delegation_ancestor_revoked" ? refusal.ancestorCid : void 0;
329
+ if (typeof bootstrapCid === "string" && (bootstrapCid === refusal.revokedCid || bootstrapCid === ancestorCid)) {
330
+ await this.#clearAuthority(tenant);
331
+ return "bootstrap";
332
+ }
333
+ const key = tenant.stateRoot;
334
+ const removed = this.#clearedScoped.get(tenant.stateRoot);
335
+ if (removed?.has(refusal.revokedCid)) return "scoped";
336
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
337
+ if (this.#clearedScoped.get(key)?.has(refusal.revokedCid)) return "scoped";
338
+ if (!Array.isArray(additional)) return void 0;
339
+ const affected = additional.some((entry) => {
340
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
341
+ return typeof cid === "string" && (cid === refusal.revokedCid || cid === ancestorCid);
342
+ });
343
+ const inProgress = this.#scopedClearing.get(key);
344
+ if (inProgress) {
345
+ await inProgress;
346
+ return this.#clearRefusedAuthority(tenant, refusal);
347
+ }
348
+ if (!affected) return void 0;
349
+ const clearing = (async () => {
350
+ const latest = await readJson(this.#additionalPath(tenant.stateRoot));
351
+ if (!Array.isArray(latest)) return;
352
+ const remaining = latest.filter((entry) => {
353
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
354
+ return typeof cid !== "string" || cid !== refusal.revokedCid && cid !== ancestorCid;
355
+ });
356
+ if (remaining.length === latest.length) return;
357
+ this.#activeSessions.delete(tenant.stateRoot);
358
+ const epoch = (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1;
359
+ this.#approvalEpochs.set(tenant.stateRoot, epoch);
360
+ const profilePath = this.#profilePath(tenant.stateRoot);
361
+ const profile = await readJson(profilePath);
362
+ await secureWrite(profilePath, { ...profile, approvalEpoch: epoch });
363
+ if (remaining.length === 0) await (0, import_promises.rm)(this.#additionalPath(tenant.stateRoot), { force: true });
364
+ else await secureWrite(this.#additionalPath(tenant.stateRoot), remaining);
365
+ const cleared = removed ?? /* @__PURE__ */ new Set();
366
+ cleared.add(refusal.revokedCid);
367
+ this.#clearedScoped.set(tenant.stateRoot, cleared);
368
+ })().finally(() => this.#scopedClearing.delete(key));
369
+ this.#scopedClearing.set(key, clearing);
370
+ await clearing;
371
+ return "scoped";
372
+ }
245
373
  async approvalRedirect(state) {
246
374
  const pending = await this.#readPending(state, false);
247
375
  const tenant = await this.#readTenant(pending.tenantId);
@@ -282,21 +410,36 @@ var RemoteTenantStore = class {
282
410
  }
283
411
  if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
284
412
  const pending = await this.#readPending(state, true);
285
- try {
286
- const tenant = await this.#readTenant(pending.tenantId);
287
- if (pending.kind === "bootstrap") {
288
- await this.#storeBootstrap(tenant, pending, data);
289
- } else {
290
- await this.#storeDelegation(tenant, pending, data);
413
+ const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", pending.tenantId);
414
+ const prior = this.#approvalProcessing.get(stateRoot);
415
+ const processing = (async () => {
416
+ if (prior) await prior.catch(() => void 0);
417
+ try {
418
+ const tenant = await this.#readTenant(pending.tenantId);
419
+ if ((pending.approvalEpoch ?? 0) !== tenant.approvalEpoch) {
420
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
421
+ }
422
+ if (pending.kind === "bootstrap") {
423
+ await this.#storeBootstrap(tenant, pending, data);
424
+ } else {
425
+ await this.#storeDelegation(tenant, pending, data);
426
+ }
427
+ await (0, import_promises.rm)(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
428
+ } catch (error) {
429
+ await this.#restorePending(pending).catch(() => void 0);
430
+ throw error;
291
431
  }
292
- await (0, import_promises.rm)(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
293
- } catch (error) {
294
- await this.#restorePending(pending).catch(() => void 0);
295
- throw error;
432
+ })();
433
+ this.#approvalProcessing.set(stateRoot, processing);
434
+ try {
435
+ await processing;
436
+ } finally {
437
+ if (this.#approvalProcessing.get(stateRoot) === processing) this.#approvalProcessing.delete(stateRoot);
296
438
  }
297
439
  }
298
440
  async #storeBootstrap(tenant, pending, data) {
299
441
  const { node, session } = await this.#restoreApproval(tenant, data);
442
+ this.#assertCurrentApproval(pending, session);
300
443
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
301
444
  throw new ApprovalOwnerMismatchError();
302
445
  }
@@ -313,10 +456,13 @@ var RemoteTenantStore = class {
313
456
  authMethod: "openkey",
314
457
  ...session
315
458
  });
459
+ this.#renewalRequired.delete(tenant.stateRoot);
460
+ this.#activeSessions.delete(tenant.stateRoot);
316
461
  }
317
462
  async #storeDelegation(tenant, pending, data) {
318
463
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
319
464
  const { node, session } = await this.#restoreApproval(tenant, data);
465
+ this.#assertCurrentApproval(pending, session);
320
466
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
321
467
  let delegation;
322
468
  try {
@@ -360,6 +506,12 @@ var RemoteTenantStore = class {
360
506
  }
361
507
  if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
362
508
  }
509
+ #assertCurrentApproval(pending, session) {
510
+ const issuedAt = /^Issued At: ([^\r\n]+)\r?$/m.exec(session.siwe)?.[1];
511
+ if (issuedAt === void 0 || !Number.isFinite(Date.parse(issuedAt)) || Date.parse(issuedAt) < Date.parse(pending.createdAt)) {
512
+ throw new ApprovalStageError("restore_approval", "APPROVAL_PREDATES_REQUEST");
513
+ }
514
+ }
363
515
  /**
364
516
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
365
517
  * callback must carry the full signed proof (SIWE, signature, address and
@@ -400,17 +552,104 @@ var RemoteTenantStore = class {
400
552
  this.#initializing.set(tenantId, initializing);
401
553
  return initializing;
402
554
  }
403
- async #hasValidSession(tenant, ownerDids) {
404
- const raw = await readJson(this.#sessionPath(tenant.stateRoot));
555
+ async #hasValidSession(tenant, ownerDids, fresh = false) {
556
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
557
+ const key = JSON.stringify([tenant.stateRoot, ownerDids.map(normalizeDid), fresh]);
558
+ const current = this.#sessionChecks.get(key);
559
+ if (current) return current;
560
+ const checking = this.#checkSession(tenant, ownerDids, fresh).finally(() => {
561
+ this.#sessionChecks.delete(key);
562
+ });
563
+ this.#sessionChecks.set(key, checking);
564
+ return checking;
565
+ }
566
+ async #checkSession(tenant, ownerDids, fresh) {
567
+ let raw;
568
+ try {
569
+ raw = await readJson(this.#sessionPath(tenant.stateRoot));
570
+ } catch (error) {
571
+ if (!(error instanceof SyntaxError)) throw error;
572
+ await this.#clearAuthority(tenant);
573
+ return false;
574
+ }
405
575
  if (raw === null) return false;
576
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
406
577
  try {
407
- const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
408
578
  await node.restoreSession(raw);
409
- return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
410
579
  } catch {
411
- await (0, import_promises.rm)(this.#sessionPath(tenant.stateRoot), { force: true });
580
+ const header = isRecord(raw.delegationHeader) ? raw.delegationHeader.Authorization : void 0;
581
+ const signedExpiry = typeof header === "string" ? (0, import_node_sdk2.signedCompactUcanExpiry)(header) : void 0;
582
+ const siweExpiry = typeof raw.siwe === "string" ? /^Expiration Time: ([^\r\n]+)\r?$/m.exec(raw.siwe)?.[1] : void 0;
583
+ const expiry = signedExpiry ?? (siweExpiry === void 0 ? void 0 : new Date(siweExpiry));
584
+ if (expiry !== void 0 && expiry.getTime() <= Date.now()) return false;
585
+ await this.#clearAuthority(tenant);
586
+ return false;
587
+ }
588
+ if (!samePrincipal(node.sessionDid, tenant.sessionDid) || !ownerDids.some((did) => samePrincipal(node.did, did))) return false;
589
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
590
+ const cids = [raw.delegationCid];
591
+ if (additional !== null) {
592
+ if (!Array.isArray(additional)) throw new Error("Stored TinyCloud authority is unavailable.");
593
+ for (const entry of additional) {
594
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
595
+ cids.push(cid);
596
+ }
597
+ }
598
+ if (!cids.every((cid) => typeof cid === "string" && cid.length > 0)) {
599
+ throw new Error("Stored TinyCloud authority is unavailable.");
600
+ }
601
+ const fingerprint = cids.join("\n");
602
+ const cached = this.#activeSessions.get(tenant.stateRoot);
603
+ if (!fresh && cached?.cids === fingerprint && cached.until > Date.now()) return true;
604
+ let timer;
605
+ const timeout = new Promise((resolve2) => {
606
+ timer = setTimeout(() => resolve2(void 0), 2e3);
607
+ timer.unref();
608
+ });
609
+ const probe = await Promise.race([
610
+ node.kvForSpace(String(raw.spaceId)).list({ prefix: "spaces/" }).catch(() => void 0),
611
+ timeout
612
+ ]).finally(() => clearTimeout(timer));
613
+ const refusal = probe !== void 0 && !probe.ok ? (0, import_mcp_authority.nodeRevocationOf)(probe.error) : void 0;
614
+ if (refusal !== void 0 && (refusal.revokedCid === raw.delegationCid || refusal.reason === "delegation_ancestor_revoked" && refusal.ancestorCid === raw.delegationCid)) {
615
+ await this.#clearAuthority(tenant);
412
616
  return false;
413
617
  }
618
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
619
+ if (probe?.ok) {
620
+ this.#activeSessions.set(tenant.stateRoot, { cids: fingerprint, until: Date.now() + 3e4 });
621
+ }
622
+ return true;
623
+ }
624
+ async #clearAuthority(tenant) {
625
+ const current = this.#clearing.get(tenant.stateRoot);
626
+ if (current) return current;
627
+ this.#activeSessions.delete(tenant.stateRoot);
628
+ this.#renewalRequired.add(tenant.stateRoot);
629
+ this.#clearedScoped.delete(tenant.stateRoot);
630
+ this.#approvalEpochs.set(
631
+ tenant.stateRoot,
632
+ (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1
633
+ );
634
+ const clearing = (async () => {
635
+ const processing = this.#approvalProcessing.get(tenant.stateRoot);
636
+ if (processing) await processing.catch(() => void 0);
637
+ const scoped = this.#scopedClearing.get(tenant.stateRoot);
638
+ if (scoped) await scoped;
639
+ this.#renewalRequired.add(tenant.stateRoot);
640
+ const profilePath = this.#profilePath(tenant.stateRoot);
641
+ const profile = await readJson(profilePath);
642
+ await secureWrite(profilePath, {
643
+ ...profile,
644
+ approvalEpoch: this.#approvalEpochs.get(tenant.stateRoot)
645
+ });
646
+ await (0, import_promises.rm)(this.#additionalPath(tenant.stateRoot), { force: true });
647
+ await (0, import_promises.rm)((0, import_node_path.join)(tenant.stateRoot, ".tinycloud", "profiles", PROFILE, "auth-requests.json"), { force: true });
648
+ await (0, import_promises.rm)(this.#pendingDirectory(tenant.stateRoot), { recursive: true, force: true });
649
+ await (0, import_promises.rm)(this.#sessionPath(tenant.stateRoot), { force: true });
650
+ })().finally(() => this.#clearing.delete(tenant.stateRoot));
651
+ this.#clearing.set(tenant.stateRoot, clearing);
652
+ await clearing;
414
653
  }
415
654
  async #initializeTenant(tenantId) {
416
655
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
@@ -449,21 +688,36 @@ var RemoteTenantStore = class {
449
688
  `
450
689
  );
451
690
  }
452
- return { stateRoot, sessionDid, jwk };
691
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
692
+ this.#approvalEpochs.set(stateRoot, approvalEpoch);
693
+ return { stateRoot, sessionDid, jwk, approvalEpoch };
453
694
  }
454
695
  async #readTenant(tenantId) {
455
696
  if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
456
697
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
457
698
  const jwk = await readJson(this.#keyPath(stateRoot));
458
699
  if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
459
- return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
700
+ const profile = await readJson(this.#profilePath(stateRoot));
701
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
702
+ return { stateRoot, jwk, sessionDid: didFromJwk(jwk), approvalEpoch };
460
703
  }
461
704
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
705
+ const clearing = this.#clearing.get(this.tenantStateRoot(subject));
706
+ if (clearing) await clearing;
462
707
  const tenant = await this.#ensureTenant(subject);
463
708
  const tenantId = this.#tenantId(subject);
709
+ const normalizedOwners = ownerDids.map(normalizeDid);
710
+ const key = JSON.stringify([tenantId, tenant.approvalEpoch, kind, requestId, permissions, normalizedOwners]);
711
+ const current = this.#pendingWrites.get(key);
712
+ if (current) return current;
713
+ const writing = this.#writePending(tenant, tenantId, kind, permissions, normalizedOwners, requestId).finally(() => this.#pendingWrites.delete(key));
714
+ this.#pendingWrites.set(key, writing);
715
+ return writing;
716
+ }
717
+ async #writePending(tenant, tenantId, kind, permissions, ownerDids, requestId) {
464
718
  const reusable = await reusablePending(
465
719
  this.#pendingDirectory(tenant.stateRoot),
466
- { tenantId, kind, requestId, permissions, ownerDids: ownerDids.map(normalizeDid) }
720
+ { tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
467
721
  );
468
722
  if (reusable !== void 0) return reusable;
469
723
  const nonce = (0, import_node_crypto.randomBytes)(24).toString("base64url");
@@ -472,10 +726,11 @@ var RemoteTenantStore = class {
472
726
  version: 1,
473
727
  nonce,
474
728
  tenantId,
729
+ approvalEpoch: tenant.approvalEpoch,
475
730
  kind,
476
731
  ...requestId === void 0 ? {} : { requestId },
477
732
  permissions,
478
- ownerDids: ownerDids.map(normalizeDid),
733
+ ownerDids,
479
734
  createdAt: new Date(created).toISOString(),
480
735
  expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
481
736
  };
@@ -516,6 +771,11 @@ var RemoteTenantStore = class {
516
771
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
517
772
  throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
518
773
  }
774
+ const profile = await readJson(this.#profilePath(stateRoot));
775
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
776
+ if ((pending.approvalEpoch ?? 0) !== approvalEpoch) {
777
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
778
+ }
519
779
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
520
780
  await (0, import_promises.rm)(consume ? claimed : path, { force: true });
521
781
  throw new ApprovalRequestError("state_expired", "Approval state has expired.");
@@ -551,6 +811,9 @@ var RemoteTenantStore = class {
551
811
  #sessionPath(stateRoot) {
552
812
  return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
553
813
  }
814
+ #additionalPath(stateRoot) {
815
+ return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "additional-delegations.json");
816
+ }
554
817
  #pendingPath(stateRoot, nonce) {
555
818
  return (0, import_node_path.join)(this.#pendingDirectory(stateRoot), `${nonce}.json`);
556
819
  }
@@ -578,6 +841,21 @@ function ensureWasm() {
578
841
  (0, import_node_sdk_wasm.initPanicHook)();
579
842
  wasmInitialized = true;
580
843
  }
844
+ function revocationFromResult(error) {
845
+ const details = error.details;
846
+ if (!isRecord(details) || typeof details.revokedCid !== "string") return void 0;
847
+ if (details.reason === "delegation_revoked") {
848
+ return { reason: "delegation_revoked", revokedCid: details.revokedCid };
849
+ }
850
+ if (details.reason === "delegation_ancestor_revoked" && typeof details.ancestorCid === "string") {
851
+ return {
852
+ reason: "delegation_ancestor_revoked",
853
+ revokedCid: details.revokedCid,
854
+ ancestorCid: details.ancestorCid
855
+ };
856
+ }
857
+ return void 0;
858
+ }
581
859
  function sessionFromCallback(data, privateJwk) {
582
860
  const delegationHeader = data.delegationHeader;
583
861
  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") {
@@ -668,7 +946,7 @@ async function reusablePending(directory, expected) {
668
946
  return void 0;
669
947
  }
670
948
  function samePendingRequest(pending, expected) {
671
- 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);
949
+ 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);
672
950
  }
673
951
 
674
952
  // src/server.ts
@@ -1034,6 +1312,7 @@ function registerTinyCloudTools(server, selection, validator) {
1034
1312
  const metrics = { replayedGrants: 0 };
1035
1313
  let code = "INTERNAL_ERROR";
1036
1314
  try {
1315
+ await selection.beforeOperation?.();
1037
1316
  const result = await (0, import_operations2.invokeOperation)(
1038
1317
  binding.operationId,
1039
1318
  binding.operationVersion,
@@ -1041,7 +1320,11 @@ function registerTinyCloudTools(server, selection, validator) {
1041
1320
  input,
1042
1321
  metrics
1043
1322
  );
1044
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1323
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result, {
1324
+ operationId: binding.operationId,
1325
+ operationVersion: binding.operationVersion,
1326
+ input
1327
+ });
1045
1328
  const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1046
1329
  surface: selection.surface,
1047
1330
  readOnly: binding.readOnlyHint,
@@ -1073,7 +1356,7 @@ function outcomeCode(result) {
1073
1356
  // package.json
1074
1357
  var package_default = {
1075
1358
  name: "@tinycloud/mcp",
1076
- version: "0.7.0-beta.35",
1359
+ version: "0.7.0-beta.36",
1077
1360
  description: "TinyCloud delegated operations MCP server",
1078
1361
  keywords: [
1079
1362
  "tinycloud",
@@ -1146,7 +1429,7 @@ var package_default = {
1146
1429
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1147
1430
  "@tinycloud/node-sdk": "3.3.0-beta.36",
1148
1431
  "@tinycloud/node-sdk-wasm": "1.7.8-beta.0",
1149
- "@tinycloud/operations": "0.7.0-beta.34",
1432
+ "@tinycloud/operations": "0.7.0-beta.35",
1150
1433
  jose: "6.2.4"
1151
1434
  },
1152
1435
  devDependencies: {
@@ -1205,7 +1488,8 @@ function createHostedMcpApp(config) {
1205
1488
  explicitProfile: true,
1206
1489
  allowOwnerProfile: false,
1207
1490
  stateRoot,
1208
- transformOperationResult: (result) => store.decorateAuthorityResult(subject, ownerDids, result)
1491
+ beforeOperation: () => store.validateTenant(subject, ownerDids),
1492
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1209
1493
  }, validator);
1210
1494
  registerConnectTool(server, validator, store, identity);
1211
1495
  return server;