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