@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/README.md +14 -0
- package/dist/cli.cjs +8 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +318 -34
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +320 -35
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +318 -34
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +320 -35
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +318 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +320 -35
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +8 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +8 -3
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +6 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +9 -2
- package/dist/tools.d.ts +9 -2
- package/dist/tools.js +6 -1
- package/dist/tools.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
223
|
+
status: "error",
|
|
224
|
+
operation: result.operation,
|
|
225
|
+
context: result.context,
|
|
196
226
|
error: {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
await this.#
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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;
|