@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/http.cjs CHANGED
@@ -121,6 +121,7 @@ var import_node_path = require("path");
121
121
  var import_node_sdk2 = require("@tinycloud/node-sdk");
122
122
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
123
123
  var import_operations = require("@tinycloud/operations");
124
+ var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
124
125
  var import_tc_apps = require("@tinycloud/operations/tc-apps");
125
126
  var PROFILE = "agent";
126
127
  var MAX_CALLBACK_BYTES = 256 * 1024;
@@ -168,6 +169,15 @@ var ApprovalRequestError = class extends Error {
168
169
  var RemoteTenantStore = class {
169
170
  #config;
170
171
  #initializing = /* @__PURE__ */ new Map();
172
+ #sessionChecks = /* @__PURE__ */ new Map();
173
+ #clearing = /* @__PURE__ */ new Map();
174
+ #activeSessions = /* @__PURE__ */ new Map();
175
+ #pendingWrites = /* @__PURE__ */ new Map();
176
+ #renewalRequired = /* @__PURE__ */ new Set();
177
+ #approvalEpochs = /* @__PURE__ */ new Map();
178
+ #scopedClearing = /* @__PURE__ */ new Map();
179
+ #clearedScoped = /* @__PURE__ */ new Map();
180
+ #approvalProcessing = /* @__PURE__ */ new Map();
171
181
  constructor(config) {
172
182
  this.#config = config;
173
183
  }
@@ -178,9 +188,13 @@ var RemoteTenantStore = class {
178
188
  async initializeTenant(subject) {
179
189
  return (await this.#ensureTenant(subject)).stateRoot;
180
190
  }
191
+ /** Check stored node authority before an authenticated operation uses it. */
192
+ async validateTenant(subject, ownerDids) {
193
+ await this.#hasValidSession(await this.#ensureTenant(subject), ownerDids);
194
+ }
181
195
  async connectStatus(subject, ownerDids = []) {
182
196
  const tenant = await this.#ensureTenant(subject);
183
- if (await this.#hasValidSession(tenant, ownerDids)) {
197
+ if (await this.#hasValidSession(tenant, ownerDids, true)) {
184
198
  return { connected: true, sessionDid: tenant.sessionDid };
185
199
  }
186
200
  const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
@@ -191,24 +205,54 @@ var RemoteTenantStore = class {
191
205
  expiresAt: pending.expiresAt
192
206
  };
193
207
  }
194
- async decorateAuthorityResult(subject, ownerDids, result) {
208
+ async decorateAuthorityResult(subject, ownerDids, result, invocation) {
195
209
  if (!isRecord(result)) return result;
196
210
  if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
197
- await this.#ensureTenant(subject);
211
+ const tenant2 = await this.#ensureTenant(subject);
212
+ const refusal = revocationFromResult(result.error);
213
+ const cleared = refusal === void 0 ? void 0 : await this.#clearRefusedAuthority(tenant2, refusal);
214
+ if (cleared === "scoped" && invocation !== void 0) {
215
+ return this.#renewScopedApproval(subject, ownerDids, tenant2, result, invocation);
216
+ }
217
+ const valid = await this.#hasValidSession(tenant2, ownerDids, true);
218
+ if (valid && result.error.code === "AUTH_REQUIRED") {
219
+ if (refusal === void 0) return result;
220
+ return {
221
+ ...result,
222
+ error: { code: "AUTH_REQUIRED", message: "TinyCloud approval is required.", retryable: false }
223
+ };
224
+ }
198
225
  const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
199
226
  return {
200
- ...result,
227
+ status: "error",
228
+ operation: result.operation,
229
+ context: result.context,
201
230
  error: {
202
- ...result.error,
203
- details: {
204
- ...isRecord(result.error.details) ? result.error.details : {},
205
- approvalUrl: this.#connectUrl(pending2),
206
- expiresAt: pending2.expiresAt
207
- }
231
+ code: result.error.code === "SESSION_NOT_FOUND" && await readJson(this.#sessionPath(tenant2.stateRoot)) === null ? "SESSION_NOT_FOUND" : "AUTH_REQUIRED",
232
+ message: "A new TinyCloud approval is required.",
233
+ retryable: false,
234
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
208
235
  }
209
236
  };
210
237
  }
211
238
  if (result.status !== "authority_required") return result;
239
+ const tenant = await this.#ensureTenant(subject);
240
+ if (this.#renewalRequired.has(tenant.stateRoot) || !await this.#hasValidSession(tenant, ownerDids, true)) {
241
+ if (this.#renewalRequired.has(tenant.stateRoot)) {
242
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
243
+ return {
244
+ status: "error",
245
+ operation: result.operation,
246
+ context: result.context,
247
+ error: {
248
+ code: "AUTH_REQUIRED",
249
+ message: "A new TinyCloud approval is required.",
250
+ retryable: false,
251
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
252
+ }
253
+ };
254
+ }
255
+ }
212
256
  const request = isRecord(result.request) ? result.request : void 0;
213
257
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
214
258
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -225,6 +269,90 @@ var RemoteTenantStore = class {
225
269
  }
226
270
  };
227
271
  }
272
+ async #renewScopedApproval(subject, ownerDids, tenant, result, invocation) {
273
+ const planned = await (0, import_operations.invokeOperation)(
274
+ "tinycloud.auth.request",
275
+ 1,
276
+ { profile: PROFILE, stateRoot: tenant.stateRoot },
277
+ invocation
278
+ );
279
+ const output = planned.status === "ok" && isRecord(planned.output) ? planned.output : void 0;
280
+ const request = isRecord(output?.request) ? output.request : void 0;
281
+ const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
282
+ const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
283
+ const missing = Array.isArray(output?.missing) ? output.missing.filter(isPermissionEntry) : [];
284
+ if (requestId === void 0 || requested.length === 0 || missing.length === 0) {
285
+ return {
286
+ status: "error",
287
+ operation: result.operation,
288
+ context: result.context,
289
+ error: { code: "AUTH_REQUIRED", message: "A new TinyCloud approval is required.", retryable: false }
290
+ };
291
+ }
292
+ const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
293
+ return {
294
+ status: "authority_required",
295
+ operation: result.operation,
296
+ context: result.context,
297
+ missing,
298
+ request,
299
+ approval: {
300
+ kind: "openkey",
301
+ requestId,
302
+ url: this.#connectUrl(pending),
303
+ fallback: (0, import_tc_apps.tcAppsApprovalMessage)(requested) ?? "Open the approval URL, then retry the same MCP tool."
304
+ },
305
+ retry: (0, import_mcp_authority.operationRetryDescriptor)(invocation, invocation.input)
306
+ };
307
+ }
308
+ async #clearRefusedAuthority(tenant, refusal) {
309
+ const raw = await readJson(this.#sessionPath(tenant.stateRoot)).catch(() => null);
310
+ const bootstrapCid = raw?.delegationCid;
311
+ const ancestorCid = refusal.reason === "delegation_ancestor_revoked" ? refusal.ancestorCid : void 0;
312
+ if (typeof bootstrapCid === "string" && (bootstrapCid === refusal.revokedCid || bootstrapCid === ancestorCid)) {
313
+ await this.#clearAuthority(tenant);
314
+ return "bootstrap";
315
+ }
316
+ const key = tenant.stateRoot;
317
+ const removed = this.#clearedScoped.get(tenant.stateRoot);
318
+ if (removed?.has(refusal.revokedCid)) return "scoped";
319
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
320
+ if (this.#clearedScoped.get(key)?.has(refusal.revokedCid)) return "scoped";
321
+ if (!Array.isArray(additional)) return void 0;
322
+ const affected = additional.some((entry) => {
323
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
324
+ return typeof cid === "string" && (cid === refusal.revokedCid || cid === ancestorCid);
325
+ });
326
+ const inProgress = this.#scopedClearing.get(key);
327
+ if (inProgress) {
328
+ await inProgress;
329
+ return this.#clearRefusedAuthority(tenant, refusal);
330
+ }
331
+ if (!affected) return void 0;
332
+ const clearing = (async () => {
333
+ const latest = await readJson(this.#additionalPath(tenant.stateRoot));
334
+ if (!Array.isArray(latest)) return;
335
+ const remaining = latest.filter((entry) => {
336
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
337
+ return typeof cid !== "string" || cid !== refusal.revokedCid && cid !== ancestorCid;
338
+ });
339
+ if (remaining.length === latest.length) return;
340
+ this.#activeSessions.delete(tenant.stateRoot);
341
+ const epoch = (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1;
342
+ this.#approvalEpochs.set(tenant.stateRoot, epoch);
343
+ const profilePath = this.#profilePath(tenant.stateRoot);
344
+ const profile = await readJson(profilePath);
345
+ await secureWrite(profilePath, { ...profile, approvalEpoch: epoch });
346
+ if (remaining.length === 0) await (0, import_promises.rm)(this.#additionalPath(tenant.stateRoot), { force: true });
347
+ else await secureWrite(this.#additionalPath(tenant.stateRoot), remaining);
348
+ const cleared = removed ?? /* @__PURE__ */ new Set();
349
+ cleared.add(refusal.revokedCid);
350
+ this.#clearedScoped.set(tenant.stateRoot, cleared);
351
+ })().finally(() => this.#scopedClearing.delete(key));
352
+ this.#scopedClearing.set(key, clearing);
353
+ await clearing;
354
+ return "scoped";
355
+ }
228
356
  async approvalRedirect(state) {
229
357
  const pending = await this.#readPending(state, false);
230
358
  const tenant = await this.#readTenant(pending.tenantId);
@@ -265,21 +393,36 @@ var RemoteTenantStore = class {
265
393
  }
266
394
  if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
267
395
  const pending = await this.#readPending(state, true);
268
- try {
269
- const tenant = await this.#readTenant(pending.tenantId);
270
- if (pending.kind === "bootstrap") {
271
- await this.#storeBootstrap(tenant, pending, data);
272
- } else {
273
- await this.#storeDelegation(tenant, pending, data);
396
+ const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", pending.tenantId);
397
+ const prior = this.#approvalProcessing.get(stateRoot);
398
+ const processing = (async () => {
399
+ if (prior) await prior.catch(() => void 0);
400
+ try {
401
+ const tenant = await this.#readTenant(pending.tenantId);
402
+ if ((pending.approvalEpoch ?? 0) !== tenant.approvalEpoch) {
403
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
404
+ }
405
+ if (pending.kind === "bootstrap") {
406
+ await this.#storeBootstrap(tenant, pending, data);
407
+ } else {
408
+ await this.#storeDelegation(tenant, pending, data);
409
+ }
410
+ await (0, import_promises.rm)(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
411
+ } catch (error) {
412
+ await this.#restorePending(pending).catch(() => void 0);
413
+ throw error;
274
414
  }
275
- await (0, import_promises.rm)(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
276
- } catch (error) {
277
- await this.#restorePending(pending).catch(() => void 0);
278
- throw error;
415
+ })();
416
+ this.#approvalProcessing.set(stateRoot, processing);
417
+ try {
418
+ await processing;
419
+ } finally {
420
+ if (this.#approvalProcessing.get(stateRoot) === processing) this.#approvalProcessing.delete(stateRoot);
279
421
  }
280
422
  }
281
423
  async #storeBootstrap(tenant, pending, data) {
282
424
  const { node, session } = await this.#restoreApproval(tenant, data);
425
+ this.#assertCurrentApproval(pending, session);
283
426
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
284
427
  throw new ApprovalOwnerMismatchError();
285
428
  }
@@ -296,10 +439,13 @@ var RemoteTenantStore = class {
296
439
  authMethod: "openkey",
297
440
  ...session
298
441
  });
442
+ this.#renewalRequired.delete(tenant.stateRoot);
443
+ this.#activeSessions.delete(tenant.stateRoot);
299
444
  }
300
445
  async #storeDelegation(tenant, pending, data) {
301
446
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
302
447
  const { node, session } = await this.#restoreApproval(tenant, data);
448
+ this.#assertCurrentApproval(pending, session);
303
449
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
304
450
  let delegation;
305
451
  try {
@@ -343,6 +489,12 @@ var RemoteTenantStore = class {
343
489
  }
344
490
  if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
345
491
  }
492
+ #assertCurrentApproval(pending, session) {
493
+ const issuedAt = /^Issued At: ([^\r\n]+)\r?$/m.exec(session.siwe)?.[1];
494
+ if (issuedAt === void 0 || !Number.isFinite(Date.parse(issuedAt)) || Date.parse(issuedAt) < Date.parse(pending.createdAt)) {
495
+ throw new ApprovalStageError("restore_approval", "APPROVAL_PREDATES_REQUEST");
496
+ }
497
+ }
346
498
  /**
347
499
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
348
500
  * callback must carry the full signed proof (SIWE, signature, address and
@@ -383,17 +535,104 @@ var RemoteTenantStore = class {
383
535
  this.#initializing.set(tenantId, initializing);
384
536
  return initializing;
385
537
  }
386
- async #hasValidSession(tenant, ownerDids) {
387
- const raw = await readJson(this.#sessionPath(tenant.stateRoot));
538
+ async #hasValidSession(tenant, ownerDids, fresh = false) {
539
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
540
+ const key = JSON.stringify([tenant.stateRoot, ownerDids.map(normalizeDid), fresh]);
541
+ const current = this.#sessionChecks.get(key);
542
+ if (current) return current;
543
+ const checking = this.#checkSession(tenant, ownerDids, fresh).finally(() => {
544
+ this.#sessionChecks.delete(key);
545
+ });
546
+ this.#sessionChecks.set(key, checking);
547
+ return checking;
548
+ }
549
+ async #checkSession(tenant, ownerDids, fresh) {
550
+ let raw;
551
+ try {
552
+ raw = await readJson(this.#sessionPath(tenant.stateRoot));
553
+ } catch (error) {
554
+ if (!(error instanceof SyntaxError)) throw error;
555
+ await this.#clearAuthority(tenant);
556
+ return false;
557
+ }
388
558
  if (raw === null) return false;
559
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
389
560
  try {
390
- const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
391
561
  await node.restoreSession(raw);
392
- return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
393
562
  } catch {
394
- await (0, import_promises.rm)(this.#sessionPath(tenant.stateRoot), { force: true });
563
+ const header = isRecord(raw.delegationHeader) ? raw.delegationHeader.Authorization : void 0;
564
+ const signedExpiry = typeof header === "string" ? (0, import_node_sdk2.signedCompactUcanExpiry)(header) : void 0;
565
+ const siweExpiry = typeof raw.siwe === "string" ? /^Expiration Time: ([^\r\n]+)\r?$/m.exec(raw.siwe)?.[1] : void 0;
566
+ const expiry = signedExpiry ?? (siweExpiry === void 0 ? void 0 : new Date(siweExpiry));
567
+ if (expiry !== void 0 && expiry.getTime() <= Date.now()) return false;
568
+ await this.#clearAuthority(tenant);
569
+ return false;
570
+ }
571
+ if (!samePrincipal(node.sessionDid, tenant.sessionDid) || !ownerDids.some((did) => samePrincipal(node.did, did))) return false;
572
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
573
+ const cids = [raw.delegationCid];
574
+ if (additional !== null) {
575
+ if (!Array.isArray(additional)) throw new Error("Stored TinyCloud authority is unavailable.");
576
+ for (const entry of additional) {
577
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
578
+ cids.push(cid);
579
+ }
580
+ }
581
+ if (!cids.every((cid) => typeof cid === "string" && cid.length > 0)) {
582
+ throw new Error("Stored TinyCloud authority is unavailable.");
583
+ }
584
+ const fingerprint = cids.join("\n");
585
+ const cached = this.#activeSessions.get(tenant.stateRoot);
586
+ if (!fresh && cached?.cids === fingerprint && cached.until > Date.now()) return true;
587
+ let timer;
588
+ const timeout = new Promise((resolve) => {
589
+ timer = setTimeout(() => resolve(void 0), 2e3);
590
+ timer.unref();
591
+ });
592
+ const probe = await Promise.race([
593
+ node.kvForSpace(String(raw.spaceId)).list({ prefix: "spaces/" }).catch(() => void 0),
594
+ timeout
595
+ ]).finally(() => clearTimeout(timer));
596
+ const refusal = probe !== void 0 && !probe.ok ? (0, import_mcp_authority.nodeRevocationOf)(probe.error) : void 0;
597
+ if (refusal !== void 0 && (refusal.revokedCid === raw.delegationCid || refusal.reason === "delegation_ancestor_revoked" && refusal.ancestorCid === raw.delegationCid)) {
598
+ await this.#clearAuthority(tenant);
395
599
  return false;
396
600
  }
601
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
602
+ if (probe?.ok) {
603
+ this.#activeSessions.set(tenant.stateRoot, { cids: fingerprint, until: Date.now() + 3e4 });
604
+ }
605
+ return true;
606
+ }
607
+ async #clearAuthority(tenant) {
608
+ const current = this.#clearing.get(tenant.stateRoot);
609
+ if (current) return current;
610
+ this.#activeSessions.delete(tenant.stateRoot);
611
+ this.#renewalRequired.add(tenant.stateRoot);
612
+ this.#clearedScoped.delete(tenant.stateRoot);
613
+ this.#approvalEpochs.set(
614
+ tenant.stateRoot,
615
+ (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1
616
+ );
617
+ const clearing = (async () => {
618
+ const processing = this.#approvalProcessing.get(tenant.stateRoot);
619
+ if (processing) await processing.catch(() => void 0);
620
+ const scoped = this.#scopedClearing.get(tenant.stateRoot);
621
+ if (scoped) await scoped;
622
+ this.#renewalRequired.add(tenant.stateRoot);
623
+ const profilePath = this.#profilePath(tenant.stateRoot);
624
+ const profile = await readJson(profilePath);
625
+ await secureWrite(profilePath, {
626
+ ...profile,
627
+ approvalEpoch: this.#approvalEpochs.get(tenant.stateRoot)
628
+ });
629
+ await (0, import_promises.rm)(this.#additionalPath(tenant.stateRoot), { force: true });
630
+ await (0, import_promises.rm)((0, import_node_path.join)(tenant.stateRoot, ".tinycloud", "profiles", PROFILE, "auth-requests.json"), { force: true });
631
+ await (0, import_promises.rm)(this.#pendingDirectory(tenant.stateRoot), { recursive: true, force: true });
632
+ await (0, import_promises.rm)(this.#sessionPath(tenant.stateRoot), { force: true });
633
+ })().finally(() => this.#clearing.delete(tenant.stateRoot));
634
+ this.#clearing.set(tenant.stateRoot, clearing);
635
+ await clearing;
397
636
  }
398
637
  async #initializeTenant(tenantId) {
399
638
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
@@ -432,21 +671,36 @@ var RemoteTenantStore = class {
432
671
  `
433
672
  );
434
673
  }
435
- return { stateRoot, sessionDid, jwk };
674
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
675
+ this.#approvalEpochs.set(stateRoot, approvalEpoch);
676
+ return { stateRoot, sessionDid, jwk, approvalEpoch };
436
677
  }
437
678
  async #readTenant(tenantId) {
438
679
  if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
439
680
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
440
681
  const jwk = await readJson(this.#keyPath(stateRoot));
441
682
  if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
442
- return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
683
+ const profile = await readJson(this.#profilePath(stateRoot));
684
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
685
+ return { stateRoot, jwk, sessionDid: didFromJwk(jwk), approvalEpoch };
443
686
  }
444
687
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
688
+ const clearing = this.#clearing.get(this.tenantStateRoot(subject));
689
+ if (clearing) await clearing;
445
690
  const tenant = await this.#ensureTenant(subject);
446
691
  const tenantId = this.#tenantId(subject);
692
+ const normalizedOwners = ownerDids.map(normalizeDid);
693
+ const key = JSON.stringify([tenantId, tenant.approvalEpoch, kind, requestId, permissions, normalizedOwners]);
694
+ const current = this.#pendingWrites.get(key);
695
+ if (current) return current;
696
+ const writing = this.#writePending(tenant, tenantId, kind, permissions, normalizedOwners, requestId).finally(() => this.#pendingWrites.delete(key));
697
+ this.#pendingWrites.set(key, writing);
698
+ return writing;
699
+ }
700
+ async #writePending(tenant, tenantId, kind, permissions, ownerDids, requestId) {
447
701
  const reusable = await reusablePending(
448
702
  this.#pendingDirectory(tenant.stateRoot),
449
- { tenantId, kind, requestId, permissions, ownerDids: ownerDids.map(normalizeDid) }
703
+ { tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
450
704
  );
451
705
  if (reusable !== void 0) return reusable;
452
706
  const nonce = (0, import_node_crypto.randomBytes)(24).toString("base64url");
@@ -455,10 +709,11 @@ var RemoteTenantStore = class {
455
709
  version: 1,
456
710
  nonce,
457
711
  tenantId,
712
+ approvalEpoch: tenant.approvalEpoch,
458
713
  kind,
459
714
  ...requestId === void 0 ? {} : { requestId },
460
715
  permissions,
461
- ownerDids: ownerDids.map(normalizeDid),
716
+ ownerDids,
462
717
  createdAt: new Date(created).toISOString(),
463
718
  expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
464
719
  };
@@ -499,6 +754,11 @@ var RemoteTenantStore = class {
499
754
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
500
755
  throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
501
756
  }
757
+ const profile = await readJson(this.#profilePath(stateRoot));
758
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
759
+ if ((pending.approvalEpoch ?? 0) !== approvalEpoch) {
760
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
761
+ }
502
762
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
503
763
  await (0, import_promises.rm)(consume ? claimed : path, { force: true });
504
764
  throw new ApprovalRequestError("state_expired", "Approval state has expired.");
@@ -534,6 +794,9 @@ var RemoteTenantStore = class {
534
794
  #sessionPath(stateRoot) {
535
795
  return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
536
796
  }
797
+ #additionalPath(stateRoot) {
798
+ return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "additional-delegations.json");
799
+ }
537
800
  #pendingPath(stateRoot, nonce) {
538
801
  return (0, import_node_path.join)(this.#pendingDirectory(stateRoot), `${nonce}.json`);
539
802
  }
@@ -561,6 +824,21 @@ function ensureWasm() {
561
824
  (0, import_node_sdk_wasm.initPanicHook)();
562
825
  wasmInitialized = true;
563
826
  }
827
+ function revocationFromResult(error) {
828
+ const details = error.details;
829
+ if (!isRecord(details) || typeof details.revokedCid !== "string") return void 0;
830
+ if (details.reason === "delegation_revoked") {
831
+ return { reason: "delegation_revoked", revokedCid: details.revokedCid };
832
+ }
833
+ if (details.reason === "delegation_ancestor_revoked" && typeof details.ancestorCid === "string") {
834
+ return {
835
+ reason: "delegation_ancestor_revoked",
836
+ revokedCid: details.revokedCid,
837
+ ancestorCid: details.ancestorCid
838
+ };
839
+ }
840
+ return void 0;
841
+ }
564
842
  function sessionFromCallback(data, privateJwk) {
565
843
  const delegationHeader = data.delegationHeader;
566
844
  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") {
@@ -651,7 +929,7 @@ async function reusablePending(directory, expected) {
651
929
  return void 0;
652
930
  }
653
931
  function samePendingRequest(pending, expected) {
654
- 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);
932
+ 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);
655
933
  }
656
934
 
657
935
  // src/server.ts
@@ -1017,6 +1295,7 @@ function registerTinyCloudTools(server, selection, validator) {
1017
1295
  const metrics = { replayedGrants: 0 };
1018
1296
  let code = "INTERNAL_ERROR";
1019
1297
  try {
1298
+ await selection.beforeOperation?.();
1020
1299
  const result = await (0, import_operations2.invokeOperation)(
1021
1300
  binding.operationId,
1022
1301
  binding.operationVersion,
@@ -1024,7 +1303,11 @@ function registerTinyCloudTools(server, selection, validator) {
1024
1303
  input,
1025
1304
  metrics
1026
1305
  );
1027
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1306
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result, {
1307
+ operationId: binding.operationId,
1308
+ operationVersion: binding.operationVersion,
1309
+ input
1310
+ });
1028
1311
  const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1029
1312
  surface: selection.surface,
1030
1313
  readOnly: binding.readOnlyHint,
@@ -1056,7 +1339,7 @@ function outcomeCode(result) {
1056
1339
  // package.json
1057
1340
  var package_default = {
1058
1341
  name: "@tinycloud/mcp",
1059
- version: "0.7.0-beta.34",
1342
+ version: "0.7.0-beta.36",
1060
1343
  description: "TinyCloud delegated operations MCP server",
1061
1344
  keywords: [
1062
1345
  "tinycloud",
@@ -1127,9 +1410,9 @@ var package_default = {
1127
1410
  dependencies: {
1128
1411
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1129
1412
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1130
- "@tinycloud/node-sdk": "3.3.0-beta.35",
1413
+ "@tinycloud/node-sdk": "3.3.0-beta.36",
1131
1414
  "@tinycloud/node-sdk-wasm": "1.7.8-beta.0",
1132
- "@tinycloud/operations": "0.7.0-beta.33",
1415
+ "@tinycloud/operations": "0.7.0-beta.35",
1133
1416
  jose: "6.2.4"
1134
1417
  },
1135
1418
  devDependencies: {
@@ -1188,7 +1471,8 @@ function createHostedMcpApp(config) {
1188
1471
  explicitProfile: true,
1189
1472
  allowOwnerProfile: false,
1190
1473
  stateRoot,
1191
- transformOperationResult: (result) => store.decorateAuthorityResult(subject, ownerDids, result)
1474
+ beforeOperation: () => store.validateTenant(subject, ownerDids),
1475
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1192
1476
  }, validator);
1193
1477
  registerConnectTool(server, validator, store, identity);
1194
1478
  return server;