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