@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/README.md +14 -0
- package/dist/cli.cjs +9 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +9 -4
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +319 -35
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +321 -36
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +319 -35
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +321 -36
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +319 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +321 -36
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +9 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +9 -4
- 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 +3 -3
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
|
-
|
|
240
|
+
status: "error",
|
|
241
|
+
operation: result.operation,
|
|
242
|
+
context: result.context,
|
|
213
243
|
error: {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await this.#
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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;
|