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

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.js CHANGED
@@ -129,10 +129,12 @@ import {
129
129
  UnsupportedSessionRestoreError,
130
130
  isCapabilitySubset,
131
131
  principalDidEquals,
132
- siweRuntimeDelegation
132
+ siweRuntimeDelegation,
133
+ signedCompactUcanExpiry
133
134
  } from "@tinycloud/node-sdk";
134
135
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
135
136
  import { invokeOperation } from "@tinycloud/operations";
137
+ import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
136
138
  import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
137
139
  var PROFILE = "agent";
138
140
  var MAX_CALLBACK_BYTES = 256 * 1024;
@@ -180,6 +182,15 @@ var ApprovalRequestError = class extends Error {
180
182
  var RemoteTenantStore = class {
181
183
  #config;
182
184
  #initializing = /* @__PURE__ */ new Map();
185
+ #sessionChecks = /* @__PURE__ */ new Map();
186
+ #clearing = /* @__PURE__ */ new Map();
187
+ #activeSessions = /* @__PURE__ */ new Map();
188
+ #pendingWrites = /* @__PURE__ */ new Map();
189
+ #renewalRequired = /* @__PURE__ */ new Set();
190
+ #approvalEpochs = /* @__PURE__ */ new Map();
191
+ #scopedClearing = /* @__PURE__ */ new Map();
192
+ #clearedScoped = /* @__PURE__ */ new Map();
193
+ #approvalProcessing = /* @__PURE__ */ new Map();
183
194
  constructor(config) {
184
195
  this.#config = config;
185
196
  }
@@ -190,9 +201,13 @@ var RemoteTenantStore = class {
190
201
  async initializeTenant(subject) {
191
202
  return (await this.#ensureTenant(subject)).stateRoot;
192
203
  }
204
+ /** Check stored node authority before an authenticated operation uses it. */
205
+ async validateTenant(subject, ownerDids) {
206
+ await this.#hasValidSession(await this.#ensureTenant(subject), ownerDids);
207
+ }
193
208
  async connectStatus(subject, ownerDids = []) {
194
209
  const tenant = await this.#ensureTenant(subject);
195
- if (await this.#hasValidSession(tenant, ownerDids)) {
210
+ if (await this.#hasValidSession(tenant, ownerDids, true)) {
196
211
  return { connected: true, sessionDid: tenant.sessionDid };
197
212
  }
198
213
  const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
@@ -203,24 +218,54 @@ var RemoteTenantStore = class {
203
218
  expiresAt: pending.expiresAt
204
219
  };
205
220
  }
206
- async decorateAuthorityResult(subject, ownerDids, result) {
221
+ async decorateAuthorityResult(subject, ownerDids, result, invocation) {
207
222
  if (!isRecord(result)) return result;
208
223
  if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
209
- await this.#ensureTenant(subject);
224
+ const tenant2 = await this.#ensureTenant(subject);
225
+ const refusal = revocationFromResult(result.error);
226
+ const cleared = refusal === void 0 ? void 0 : await this.#clearRefusedAuthority(tenant2, refusal);
227
+ if (cleared === "scoped" && invocation !== void 0) {
228
+ return this.#renewScopedApproval(subject, ownerDids, tenant2, result, invocation);
229
+ }
230
+ const valid = await this.#hasValidSession(tenant2, ownerDids, true);
231
+ if (valid && result.error.code === "AUTH_REQUIRED") {
232
+ if (refusal === void 0) return result;
233
+ return {
234
+ ...result,
235
+ error: { code: "AUTH_REQUIRED", message: "TinyCloud approval is required.", retryable: false }
236
+ };
237
+ }
210
238
  const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
211
239
  return {
212
- ...result,
240
+ status: "error",
241
+ operation: result.operation,
242
+ context: result.context,
213
243
  error: {
214
- ...result.error,
215
- details: {
216
- ...isRecord(result.error.details) ? result.error.details : {},
217
- approvalUrl: this.#connectUrl(pending2),
218
- expiresAt: pending2.expiresAt
219
- }
244
+ code: result.error.code === "SESSION_NOT_FOUND" && await readJson(this.#sessionPath(tenant2.stateRoot)) === null ? "SESSION_NOT_FOUND" : "AUTH_REQUIRED",
245
+ message: "A new TinyCloud approval is required.",
246
+ retryable: false,
247
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
220
248
  }
221
249
  };
222
250
  }
223
251
  if (result.status !== "authority_required") return result;
252
+ const tenant = await this.#ensureTenant(subject);
253
+ if (this.#renewalRequired.has(tenant.stateRoot) || !await this.#hasValidSession(tenant, ownerDids, true)) {
254
+ if (this.#renewalRequired.has(tenant.stateRoot)) {
255
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
256
+ return {
257
+ status: "error",
258
+ operation: result.operation,
259
+ context: result.context,
260
+ error: {
261
+ code: "AUTH_REQUIRED",
262
+ message: "A new TinyCloud approval is required.",
263
+ retryable: false,
264
+ details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
265
+ }
266
+ };
267
+ }
268
+ }
224
269
  const request = isRecord(result.request) ? result.request : void 0;
225
270
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
226
271
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -237,6 +282,90 @@ var RemoteTenantStore = class {
237
282
  }
238
283
  };
239
284
  }
285
+ async #renewScopedApproval(subject, ownerDids, tenant, result, invocation) {
286
+ const planned = await invokeOperation(
287
+ "tinycloud.auth.request",
288
+ 1,
289
+ { profile: PROFILE, stateRoot: tenant.stateRoot },
290
+ invocation
291
+ );
292
+ const output = planned.status === "ok" && isRecord(planned.output) ? planned.output : void 0;
293
+ const request = isRecord(output?.request) ? output.request : void 0;
294
+ const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
295
+ const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
296
+ const missing = Array.isArray(output?.missing) ? output.missing.filter(isPermissionEntry) : [];
297
+ if (requestId === void 0 || requested.length === 0 || missing.length === 0) {
298
+ return {
299
+ status: "error",
300
+ operation: result.operation,
301
+ context: result.context,
302
+ error: { code: "AUTH_REQUIRED", message: "A new TinyCloud approval is required.", retryable: false }
303
+ };
304
+ }
305
+ const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
306
+ return {
307
+ status: "authority_required",
308
+ operation: result.operation,
309
+ context: result.context,
310
+ missing,
311
+ request,
312
+ approval: {
313
+ kind: "openkey",
314
+ requestId,
315
+ url: this.#connectUrl(pending),
316
+ fallback: tcAppsApprovalMessage(requested) ?? "Open the approval URL, then retry the same MCP tool."
317
+ },
318
+ retry: operationRetryDescriptor(invocation, invocation.input)
319
+ };
320
+ }
321
+ async #clearRefusedAuthority(tenant, refusal) {
322
+ const raw = await readJson(this.#sessionPath(tenant.stateRoot)).catch(() => null);
323
+ const bootstrapCid = raw?.delegationCid;
324
+ const ancestorCid = refusal.reason === "delegation_ancestor_revoked" ? refusal.ancestorCid : void 0;
325
+ if (typeof bootstrapCid === "string" && (bootstrapCid === refusal.revokedCid || bootstrapCid === ancestorCid)) {
326
+ await this.#clearAuthority(tenant);
327
+ return "bootstrap";
328
+ }
329
+ const key = tenant.stateRoot;
330
+ const removed = this.#clearedScoped.get(tenant.stateRoot);
331
+ if (removed?.has(refusal.revokedCid)) return "scoped";
332
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
333
+ if (this.#clearedScoped.get(key)?.has(refusal.revokedCid)) return "scoped";
334
+ if (!Array.isArray(additional)) return void 0;
335
+ const affected = additional.some((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
+ const inProgress = this.#scopedClearing.get(key);
340
+ if (inProgress) {
341
+ await inProgress;
342
+ return this.#clearRefusedAuthority(tenant, refusal);
343
+ }
344
+ if (!affected) return void 0;
345
+ const clearing = (async () => {
346
+ const latest = await readJson(this.#additionalPath(tenant.stateRoot));
347
+ if (!Array.isArray(latest)) return;
348
+ const remaining = latest.filter((entry) => {
349
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
350
+ return typeof cid !== "string" || cid !== refusal.revokedCid && cid !== ancestorCid;
351
+ });
352
+ if (remaining.length === latest.length) return;
353
+ this.#activeSessions.delete(tenant.stateRoot);
354
+ const epoch = (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1;
355
+ this.#approvalEpochs.set(tenant.stateRoot, epoch);
356
+ const profilePath = this.#profilePath(tenant.stateRoot);
357
+ const profile = await readJson(profilePath);
358
+ await secureWrite(profilePath, { ...profile, approvalEpoch: epoch });
359
+ if (remaining.length === 0) await rm(this.#additionalPath(tenant.stateRoot), { force: true });
360
+ else await secureWrite(this.#additionalPath(tenant.stateRoot), remaining);
361
+ const cleared = removed ?? /* @__PURE__ */ new Set();
362
+ cleared.add(refusal.revokedCid);
363
+ this.#clearedScoped.set(tenant.stateRoot, cleared);
364
+ })().finally(() => this.#scopedClearing.delete(key));
365
+ this.#scopedClearing.set(key, clearing);
366
+ await clearing;
367
+ return "scoped";
368
+ }
240
369
  async approvalRedirect(state) {
241
370
  const pending = await this.#readPending(state, false);
242
371
  const tenant = await this.#readTenant(pending.tenantId);
@@ -277,21 +406,36 @@ var RemoteTenantStore = class {
277
406
  }
278
407
  if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
279
408
  const pending = await this.#readPending(state, true);
280
- try {
281
- const tenant = await this.#readTenant(pending.tenantId);
282
- if (pending.kind === "bootstrap") {
283
- await this.#storeBootstrap(tenant, pending, data);
284
- } else {
285
- await this.#storeDelegation(tenant, pending, data);
409
+ const stateRoot = join(this.#config.stateDir, "tenants", pending.tenantId);
410
+ const prior = this.#approvalProcessing.get(stateRoot);
411
+ const processing = (async () => {
412
+ if (prior) await prior.catch(() => void 0);
413
+ try {
414
+ const tenant = await this.#readTenant(pending.tenantId);
415
+ if ((pending.approvalEpoch ?? 0) !== tenant.approvalEpoch) {
416
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
417
+ }
418
+ if (pending.kind === "bootstrap") {
419
+ await this.#storeBootstrap(tenant, pending, data);
420
+ } else {
421
+ await this.#storeDelegation(tenant, pending, data);
422
+ }
423
+ await rm(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
424
+ } catch (error) {
425
+ await this.#restorePending(pending).catch(() => void 0);
426
+ throw error;
286
427
  }
287
- await rm(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
288
- } catch (error) {
289
- await this.#restorePending(pending).catch(() => void 0);
290
- throw error;
428
+ })();
429
+ this.#approvalProcessing.set(stateRoot, processing);
430
+ try {
431
+ await processing;
432
+ } finally {
433
+ if (this.#approvalProcessing.get(stateRoot) === processing) this.#approvalProcessing.delete(stateRoot);
291
434
  }
292
435
  }
293
436
  async #storeBootstrap(tenant, pending, data) {
294
437
  const { node, session } = await this.#restoreApproval(tenant, data);
438
+ this.#assertCurrentApproval(pending, session);
295
439
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
296
440
  throw new ApprovalOwnerMismatchError();
297
441
  }
@@ -308,10 +452,13 @@ var RemoteTenantStore = class {
308
452
  authMethod: "openkey",
309
453
  ...session
310
454
  });
455
+ this.#renewalRequired.delete(tenant.stateRoot);
456
+ this.#activeSessions.delete(tenant.stateRoot);
311
457
  }
312
458
  async #storeDelegation(tenant, pending, data) {
313
459
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
314
460
  const { node, session } = await this.#restoreApproval(tenant, data);
461
+ this.#assertCurrentApproval(pending, session);
315
462
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
316
463
  let delegation;
317
464
  try {
@@ -355,6 +502,12 @@ var RemoteTenantStore = class {
355
502
  }
356
503
  if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
357
504
  }
505
+ #assertCurrentApproval(pending, session) {
506
+ const issuedAt = /^Issued At: ([^\r\n]+)\r?$/m.exec(session.siwe)?.[1];
507
+ if (issuedAt === void 0 || !Number.isFinite(Date.parse(issuedAt)) || Date.parse(issuedAt) < Date.parse(pending.createdAt)) {
508
+ throw new ApprovalStageError("restore_approval", "APPROVAL_PREDATES_REQUEST");
509
+ }
510
+ }
358
511
  /**
359
512
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
360
513
  * callback must carry the full signed proof (SIWE, signature, address and
@@ -395,17 +548,104 @@ var RemoteTenantStore = class {
395
548
  this.#initializing.set(tenantId, initializing);
396
549
  return initializing;
397
550
  }
398
- async #hasValidSession(tenant, ownerDids) {
399
- const raw = await readJson(this.#sessionPath(tenant.stateRoot));
551
+ async #hasValidSession(tenant, ownerDids, fresh = false) {
552
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
553
+ const key = JSON.stringify([tenant.stateRoot, ownerDids.map(normalizeDid), fresh]);
554
+ const current = this.#sessionChecks.get(key);
555
+ if (current) return current;
556
+ const checking = this.#checkSession(tenant, ownerDids, fresh).finally(() => {
557
+ this.#sessionChecks.delete(key);
558
+ });
559
+ this.#sessionChecks.set(key, checking);
560
+ return checking;
561
+ }
562
+ async #checkSession(tenant, ownerDids, fresh) {
563
+ let raw;
564
+ try {
565
+ raw = await readJson(this.#sessionPath(tenant.stateRoot));
566
+ } catch (error) {
567
+ if (!(error instanceof SyntaxError)) throw error;
568
+ await this.#clearAuthority(tenant);
569
+ return false;
570
+ }
400
571
  if (raw === null) return false;
572
+ const node = new TinyCloudNode({ host: this.#config.nodeHost });
401
573
  try {
402
- const node = new TinyCloudNode({ host: this.#config.nodeHost });
403
574
  await node.restoreSession(raw);
404
- return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
405
575
  } catch {
406
- await rm(this.#sessionPath(tenant.stateRoot), { force: true });
576
+ const header = isRecord(raw.delegationHeader) ? raw.delegationHeader.Authorization : void 0;
577
+ const signedExpiry = typeof header === "string" ? signedCompactUcanExpiry(header) : void 0;
578
+ const siweExpiry = typeof raw.siwe === "string" ? /^Expiration Time: ([^\r\n]+)\r?$/m.exec(raw.siwe)?.[1] : void 0;
579
+ const expiry = signedExpiry ?? (siweExpiry === void 0 ? void 0 : new Date(siweExpiry));
580
+ if (expiry !== void 0 && expiry.getTime() <= Date.now()) return false;
581
+ await this.#clearAuthority(tenant);
582
+ return false;
583
+ }
584
+ if (!samePrincipal(node.sessionDid, tenant.sessionDid) || !ownerDids.some((did) => samePrincipal(node.did, did))) return false;
585
+ const additional = await readJson(this.#additionalPath(tenant.stateRoot));
586
+ const cids = [raw.delegationCid];
587
+ if (additional !== null) {
588
+ if (!Array.isArray(additional)) throw new Error("Stored TinyCloud authority is unavailable.");
589
+ for (const entry of additional) {
590
+ const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
591
+ cids.push(cid);
592
+ }
593
+ }
594
+ if (!cids.every((cid) => typeof cid === "string" && cid.length > 0)) {
595
+ throw new Error("Stored TinyCloud authority is unavailable.");
596
+ }
597
+ const fingerprint = cids.join("\n");
598
+ const cached = this.#activeSessions.get(tenant.stateRoot);
599
+ if (!fresh && cached?.cids === fingerprint && cached.until > Date.now()) return true;
600
+ let timer;
601
+ const timeout = new Promise((resolve2) => {
602
+ timer = setTimeout(() => resolve2(void 0), 2e3);
603
+ timer.unref();
604
+ });
605
+ const probe = await Promise.race([
606
+ node.kvForSpace(String(raw.spaceId)).list({ prefix: "spaces/" }).catch(() => void 0),
607
+ timeout
608
+ ]).finally(() => clearTimeout(timer));
609
+ const refusal = probe !== void 0 && !probe.ok ? nodeRevocationOf(probe.error) : void 0;
610
+ if (refusal !== void 0 && (refusal.revokedCid === raw.delegationCid || refusal.reason === "delegation_ancestor_revoked" && refusal.ancestorCid === raw.delegationCid)) {
611
+ await this.#clearAuthority(tenant);
407
612
  return false;
408
613
  }
614
+ if (this.#renewalRequired.has(tenant.stateRoot)) return false;
615
+ if (probe?.ok) {
616
+ this.#activeSessions.set(tenant.stateRoot, { cids: fingerprint, until: Date.now() + 3e4 });
617
+ }
618
+ return true;
619
+ }
620
+ async #clearAuthority(tenant) {
621
+ const current = this.#clearing.get(tenant.stateRoot);
622
+ if (current) return current;
623
+ this.#activeSessions.delete(tenant.stateRoot);
624
+ this.#renewalRequired.add(tenant.stateRoot);
625
+ this.#clearedScoped.delete(tenant.stateRoot);
626
+ this.#approvalEpochs.set(
627
+ tenant.stateRoot,
628
+ (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1
629
+ );
630
+ const clearing = (async () => {
631
+ const processing = this.#approvalProcessing.get(tenant.stateRoot);
632
+ if (processing) await processing.catch(() => void 0);
633
+ const scoped = this.#scopedClearing.get(tenant.stateRoot);
634
+ if (scoped) await scoped;
635
+ this.#renewalRequired.add(tenant.stateRoot);
636
+ const profilePath = this.#profilePath(tenant.stateRoot);
637
+ const profile = await readJson(profilePath);
638
+ await secureWrite(profilePath, {
639
+ ...profile,
640
+ approvalEpoch: this.#approvalEpochs.get(tenant.stateRoot)
641
+ });
642
+ await rm(this.#additionalPath(tenant.stateRoot), { force: true });
643
+ await rm(join(tenant.stateRoot, ".tinycloud", "profiles", PROFILE, "auth-requests.json"), { force: true });
644
+ await rm(this.#pendingDirectory(tenant.stateRoot), { recursive: true, force: true });
645
+ await rm(this.#sessionPath(tenant.stateRoot), { force: true });
646
+ })().finally(() => this.#clearing.delete(tenant.stateRoot));
647
+ this.#clearing.set(tenant.stateRoot, clearing);
648
+ await clearing;
409
649
  }
410
650
  async #initializeTenant(tenantId) {
411
651
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
@@ -444,21 +684,36 @@ var RemoteTenantStore = class {
444
684
  `
445
685
  );
446
686
  }
447
- return { stateRoot, sessionDid, jwk };
687
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
688
+ this.#approvalEpochs.set(stateRoot, approvalEpoch);
689
+ return { stateRoot, sessionDid, jwk, approvalEpoch };
448
690
  }
449
691
  async #readTenant(tenantId) {
450
692
  if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
451
693
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
452
694
  const jwk = await readJson(this.#keyPath(stateRoot));
453
695
  if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
454
- return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
696
+ const profile = await readJson(this.#profilePath(stateRoot));
697
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
698
+ return { stateRoot, jwk, sessionDid: didFromJwk(jwk), approvalEpoch };
455
699
  }
456
700
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
701
+ const clearing = this.#clearing.get(this.tenantStateRoot(subject));
702
+ if (clearing) await clearing;
457
703
  const tenant = await this.#ensureTenant(subject);
458
704
  const tenantId = this.#tenantId(subject);
705
+ const normalizedOwners = ownerDids.map(normalizeDid);
706
+ const key = JSON.stringify([tenantId, tenant.approvalEpoch, kind, requestId, permissions, normalizedOwners]);
707
+ const current = this.#pendingWrites.get(key);
708
+ if (current) return current;
709
+ const writing = this.#writePending(tenant, tenantId, kind, permissions, normalizedOwners, requestId).finally(() => this.#pendingWrites.delete(key));
710
+ this.#pendingWrites.set(key, writing);
711
+ return writing;
712
+ }
713
+ async #writePending(tenant, tenantId, kind, permissions, ownerDids, requestId) {
459
714
  const reusable = await reusablePending(
460
715
  this.#pendingDirectory(tenant.stateRoot),
461
- { tenantId, kind, requestId, permissions, ownerDids: ownerDids.map(normalizeDid) }
716
+ { tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
462
717
  );
463
718
  if (reusable !== void 0) return reusable;
464
719
  const nonce = randomBytes(24).toString("base64url");
@@ -467,10 +722,11 @@ var RemoteTenantStore = class {
467
722
  version: 1,
468
723
  nonce,
469
724
  tenantId,
725
+ approvalEpoch: tenant.approvalEpoch,
470
726
  kind,
471
727
  ...requestId === void 0 ? {} : { requestId },
472
728
  permissions,
473
- ownerDids: ownerDids.map(normalizeDid),
729
+ ownerDids,
474
730
  createdAt: new Date(created).toISOString(),
475
731
  expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
476
732
  };
@@ -511,6 +767,11 @@ var RemoteTenantStore = class {
511
767
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
512
768
  throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
513
769
  }
770
+ const profile = await readJson(this.#profilePath(stateRoot));
771
+ const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
772
+ if ((pending.approvalEpoch ?? 0) !== approvalEpoch) {
773
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
774
+ }
514
775
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
515
776
  await rm(consume ? claimed : path, { force: true });
516
777
  throw new ApprovalRequestError("state_expired", "Approval state has expired.");
@@ -546,6 +807,9 @@ var RemoteTenantStore = class {
546
807
  #sessionPath(stateRoot) {
547
808
  return join(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
548
809
  }
810
+ #additionalPath(stateRoot) {
811
+ return join(stateRoot, ".tinycloud", "profiles", PROFILE, "additional-delegations.json");
812
+ }
549
813
  #pendingPath(stateRoot, nonce) {
550
814
  return join(this.#pendingDirectory(stateRoot), `${nonce}.json`);
551
815
  }
@@ -573,6 +837,21 @@ function ensureWasm() {
573
837
  initPanicHook();
574
838
  wasmInitialized = true;
575
839
  }
840
+ function revocationFromResult(error) {
841
+ const details = error.details;
842
+ if (!isRecord(details) || typeof details.revokedCid !== "string") return void 0;
843
+ if (details.reason === "delegation_revoked") {
844
+ return { reason: "delegation_revoked", revokedCid: details.revokedCid };
845
+ }
846
+ if (details.reason === "delegation_ancestor_revoked" && typeof details.ancestorCid === "string") {
847
+ return {
848
+ reason: "delegation_ancestor_revoked",
849
+ revokedCid: details.revokedCid,
850
+ ancestorCid: details.ancestorCid
851
+ };
852
+ }
853
+ return void 0;
854
+ }
576
855
  function sessionFromCallback(data, privateJwk) {
577
856
  const delegationHeader = data.delegationHeader;
578
857
  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") {
@@ -663,7 +942,7 @@ async function reusablePending(directory, expected) {
663
942
  return void 0;
664
943
  }
665
944
  function samePendingRequest(pending, expected) {
666
- 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);
945
+ 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);
667
946
  }
668
947
 
669
948
  // src/server.ts
@@ -1028,6 +1307,7 @@ function registerTinyCloudTools(server, selection, validator) {
1028
1307
  const metrics = { replayedGrants: 0 };
1029
1308
  let code = "INTERNAL_ERROR";
1030
1309
  try {
1310
+ await selection.beforeOperation?.();
1031
1311
  const result = await invokeOperation2(
1032
1312
  binding.operationId,
1033
1313
  binding.operationVersion,
@@ -1035,7 +1315,11 @@ function registerTinyCloudTools(server, selection, validator) {
1035
1315
  input,
1036
1316
  metrics
1037
1317
  );
1038
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1318
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result, {
1319
+ operationId: binding.operationId,
1320
+ operationVersion: binding.operationVersion,
1321
+ input
1322
+ });
1039
1323
  const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1040
1324
  surface: selection.surface,
1041
1325
  readOnly: binding.readOnlyHint,
@@ -1067,7 +1351,7 @@ function outcomeCode(result) {
1067
1351
  // package.json
1068
1352
  var package_default = {
1069
1353
  name: "@tinycloud/mcp",
1070
- version: "0.7.0-beta.35",
1354
+ version: "0.7.0-beta.37",
1071
1355
  description: "TinyCloud delegated operations MCP server",
1072
1356
  keywords: [
1073
1357
  "tinycloud",
@@ -1138,9 +1422,9 @@ var package_default = {
1138
1422
  dependencies: {
1139
1423
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1140
1424
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1141
- "@tinycloud/node-sdk": "3.3.0-beta.36",
1425
+ "@tinycloud/node-sdk": "3.3.0-beta.37",
1142
1426
  "@tinycloud/node-sdk-wasm": "1.7.8-beta.0",
1143
- "@tinycloud/operations": "0.7.0-beta.34",
1427
+ "@tinycloud/operations": "0.7.0-beta.36",
1144
1428
  jose: "6.2.4"
1145
1429
  },
1146
1430
  devDependencies: {
@@ -1199,7 +1483,8 @@ function createHostedMcpApp(config) {
1199
1483
  explicitProfile: true,
1200
1484
  allowOwnerProfile: false,
1201
1485
  stateRoot,
1202
- transformOperationResult: (result) => store.decorateAuthorityResult(subject, ownerDids, result)
1486
+ beforeOperation: () => store.validateTenant(subject, ownerDids),
1487
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1203
1488
  }, validator);
1204
1489
  registerConnectTool(server, validator, store, identity);
1205
1490
  return server;