@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/index.cjs
CHANGED
|
@@ -440,6 +440,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
440
440
|
const metrics = { replayedGrants: 0 };
|
|
441
441
|
let code = "INTERNAL_ERROR";
|
|
442
442
|
try {
|
|
443
|
+
await selection.beforeOperation?.();
|
|
443
444
|
const result = await (0, import_operations.invokeOperation)(
|
|
444
445
|
binding.operationId,
|
|
445
446
|
binding.operationVersion,
|
|
@@ -447,7 +448,11 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
447
448
|
input,
|
|
448
449
|
metrics
|
|
449
450
|
);
|
|
450
|
-
const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result
|
|
451
|
+
const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result, {
|
|
452
|
+
operationId: binding.operationId,
|
|
453
|
+
operationVersion: binding.operationVersion,
|
|
454
|
+
input
|
|
455
|
+
});
|
|
451
456
|
const response = toMcpToolResult(projected, TOOL_BINDINGS, {
|
|
452
457
|
surface: selection.surface,
|
|
453
458
|
readOnly: binding.readOnlyHint,
|
|
@@ -479,7 +484,7 @@ function outcomeCode(result) {
|
|
|
479
484
|
// package.json
|
|
480
485
|
var package_default = {
|
|
481
486
|
name: "@tinycloud/mcp",
|
|
482
|
-
version: "0.7.0-beta.
|
|
487
|
+
version: "0.7.0-beta.37",
|
|
483
488
|
description: "TinyCloud delegated operations MCP server",
|
|
484
489
|
keywords: [
|
|
485
490
|
"tinycloud",
|
|
@@ -550,9 +555,9 @@ var package_default = {
|
|
|
550
555
|
dependencies: {
|
|
551
556
|
"@modelcontextprotocol/node": "2.0.0-beta.5",
|
|
552
557
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
553
|
-
"@tinycloud/node-sdk": "3.3.0-beta.
|
|
558
|
+
"@tinycloud/node-sdk": "3.3.0-beta.37",
|
|
554
559
|
"@tinycloud/node-sdk-wasm": "1.7.8-beta.0",
|
|
555
|
-
"@tinycloud/operations": "0.7.0-beta.
|
|
560
|
+
"@tinycloud/operations": "0.7.0-beta.36",
|
|
556
561
|
jose: "6.2.4"
|
|
557
562
|
},
|
|
558
563
|
devDependencies: {
|
|
@@ -778,6 +783,7 @@ var import_node_path3 = require("path");
|
|
|
778
783
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
779
784
|
var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
|
|
780
785
|
var import_operations2 = require("@tinycloud/operations");
|
|
786
|
+
var import_mcp_authority = require("@tinycloud/operations/mcp-authority");
|
|
781
787
|
var import_tc_apps = require("@tinycloud/operations/tc-apps");
|
|
782
788
|
var PROFILE = "agent";
|
|
783
789
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
@@ -825,6 +831,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
825
831
|
var RemoteTenantStore = class {
|
|
826
832
|
#config;
|
|
827
833
|
#initializing = /* @__PURE__ */ new Map();
|
|
834
|
+
#sessionChecks = /* @__PURE__ */ new Map();
|
|
835
|
+
#clearing = /* @__PURE__ */ new Map();
|
|
836
|
+
#activeSessions = /* @__PURE__ */ new Map();
|
|
837
|
+
#pendingWrites = /* @__PURE__ */ new Map();
|
|
838
|
+
#renewalRequired = /* @__PURE__ */ new Set();
|
|
839
|
+
#approvalEpochs = /* @__PURE__ */ new Map();
|
|
840
|
+
#scopedClearing = /* @__PURE__ */ new Map();
|
|
841
|
+
#clearedScoped = /* @__PURE__ */ new Map();
|
|
842
|
+
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
828
843
|
constructor(config) {
|
|
829
844
|
this.#config = config;
|
|
830
845
|
}
|
|
@@ -835,9 +850,13 @@ var RemoteTenantStore = class {
|
|
|
835
850
|
async initializeTenant(subject) {
|
|
836
851
|
return (await this.#ensureTenant(subject)).stateRoot;
|
|
837
852
|
}
|
|
853
|
+
/** Check stored node authority before an authenticated operation uses it. */
|
|
854
|
+
async validateTenant(subject, ownerDids) {
|
|
855
|
+
await this.#hasValidSession(await this.#ensureTenant(subject), ownerDids);
|
|
856
|
+
}
|
|
838
857
|
async connectStatus(subject, ownerDids = []) {
|
|
839
858
|
const tenant = await this.#ensureTenant(subject);
|
|
840
|
-
if (await this.#hasValidSession(tenant, ownerDids)) {
|
|
859
|
+
if (await this.#hasValidSession(tenant, ownerDids, true)) {
|
|
841
860
|
return { connected: true, sessionDid: tenant.sessionDid };
|
|
842
861
|
}
|
|
843
862
|
const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
|
|
@@ -848,24 +867,54 @@ var RemoteTenantStore = class {
|
|
|
848
867
|
expiresAt: pending.expiresAt
|
|
849
868
|
};
|
|
850
869
|
}
|
|
851
|
-
async decorateAuthorityResult(subject, ownerDids, result) {
|
|
870
|
+
async decorateAuthorityResult(subject, ownerDids, result, invocation) {
|
|
852
871
|
if (!isRecord(result)) return result;
|
|
853
872
|
if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
|
|
854
|
-
await this.#ensureTenant(subject);
|
|
873
|
+
const tenant2 = await this.#ensureTenant(subject);
|
|
874
|
+
const refusal = revocationFromResult(result.error);
|
|
875
|
+
const cleared = refusal === void 0 ? void 0 : await this.#clearRefusedAuthority(tenant2, refusal);
|
|
876
|
+
if (cleared === "scoped" && invocation !== void 0) {
|
|
877
|
+
return this.#renewScopedApproval(subject, ownerDids, tenant2, result, invocation);
|
|
878
|
+
}
|
|
879
|
+
const valid = await this.#hasValidSession(tenant2, ownerDids, true);
|
|
880
|
+
if (valid && result.error.code === "AUTH_REQUIRED") {
|
|
881
|
+
if (refusal === void 0) return result;
|
|
882
|
+
return {
|
|
883
|
+
...result,
|
|
884
|
+
error: { code: "AUTH_REQUIRED", message: "TinyCloud approval is required.", retryable: false }
|
|
885
|
+
};
|
|
886
|
+
}
|
|
855
887
|
const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
|
|
856
888
|
return {
|
|
857
|
-
|
|
889
|
+
status: "error",
|
|
890
|
+
operation: result.operation,
|
|
891
|
+
context: result.context,
|
|
858
892
|
error: {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
expiresAt: pending2.expiresAt
|
|
864
|
-
}
|
|
893
|
+
code: result.error.code === "SESSION_NOT_FOUND" && await readJson(this.#sessionPath(tenant2.stateRoot)) === null ? "SESSION_NOT_FOUND" : "AUTH_REQUIRED",
|
|
894
|
+
message: "A new TinyCloud approval is required.",
|
|
895
|
+
retryable: false,
|
|
896
|
+
details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
|
|
865
897
|
}
|
|
866
898
|
};
|
|
867
899
|
}
|
|
868
900
|
if (result.status !== "authority_required") return result;
|
|
901
|
+
const tenant = await this.#ensureTenant(subject);
|
|
902
|
+
if (this.#renewalRequired.has(tenant.stateRoot) || !await this.#hasValidSession(tenant, ownerDids, true)) {
|
|
903
|
+
if (this.#renewalRequired.has(tenant.stateRoot)) {
|
|
904
|
+
const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
|
|
905
|
+
return {
|
|
906
|
+
status: "error",
|
|
907
|
+
operation: result.operation,
|
|
908
|
+
context: result.context,
|
|
909
|
+
error: {
|
|
910
|
+
code: "AUTH_REQUIRED",
|
|
911
|
+
message: "A new TinyCloud approval is required.",
|
|
912
|
+
retryable: false,
|
|
913
|
+
details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
869
918
|
const request = isRecord(result.request) ? result.request : void 0;
|
|
870
919
|
const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
|
|
871
920
|
const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
|
|
@@ -882,6 +931,90 @@ var RemoteTenantStore = class {
|
|
|
882
931
|
}
|
|
883
932
|
};
|
|
884
933
|
}
|
|
934
|
+
async #renewScopedApproval(subject, ownerDids, tenant, result, invocation) {
|
|
935
|
+
const planned = await (0, import_operations2.invokeOperation)(
|
|
936
|
+
"tinycloud.auth.request",
|
|
937
|
+
1,
|
|
938
|
+
{ profile: PROFILE, stateRoot: tenant.stateRoot },
|
|
939
|
+
invocation
|
|
940
|
+
);
|
|
941
|
+
const output = planned.status === "ok" && isRecord(planned.output) ? planned.output : void 0;
|
|
942
|
+
const request = isRecord(output?.request) ? output.request : void 0;
|
|
943
|
+
const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
|
|
944
|
+
const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
|
|
945
|
+
const missing = Array.isArray(output?.missing) ? output.missing.filter(isPermissionEntry) : [];
|
|
946
|
+
if (requestId === void 0 || requested.length === 0 || missing.length === 0) {
|
|
947
|
+
return {
|
|
948
|
+
status: "error",
|
|
949
|
+
operation: result.operation,
|
|
950
|
+
context: result.context,
|
|
951
|
+
error: { code: "AUTH_REQUIRED", message: "A new TinyCloud approval is required.", retryable: false }
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
|
|
955
|
+
return {
|
|
956
|
+
status: "authority_required",
|
|
957
|
+
operation: result.operation,
|
|
958
|
+
context: result.context,
|
|
959
|
+
missing,
|
|
960
|
+
request,
|
|
961
|
+
approval: {
|
|
962
|
+
kind: "openkey",
|
|
963
|
+
requestId,
|
|
964
|
+
url: this.#connectUrl(pending),
|
|
965
|
+
fallback: (0, import_tc_apps.tcAppsApprovalMessage)(requested) ?? "Open the approval URL, then retry the same MCP tool."
|
|
966
|
+
},
|
|
967
|
+
retry: (0, import_mcp_authority.operationRetryDescriptor)(invocation, invocation.input)
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async #clearRefusedAuthority(tenant, refusal) {
|
|
971
|
+
const raw = await readJson(this.#sessionPath(tenant.stateRoot)).catch(() => null);
|
|
972
|
+
const bootstrapCid = raw?.delegationCid;
|
|
973
|
+
const ancestorCid = refusal.reason === "delegation_ancestor_revoked" ? refusal.ancestorCid : void 0;
|
|
974
|
+
if (typeof bootstrapCid === "string" && (bootstrapCid === refusal.revokedCid || bootstrapCid === ancestorCid)) {
|
|
975
|
+
await this.#clearAuthority(tenant);
|
|
976
|
+
return "bootstrap";
|
|
977
|
+
}
|
|
978
|
+
const key = tenant.stateRoot;
|
|
979
|
+
const removed = this.#clearedScoped.get(tenant.stateRoot);
|
|
980
|
+
if (removed?.has(refusal.revokedCid)) return "scoped";
|
|
981
|
+
const additional = await readJson(this.#additionalPath(tenant.stateRoot));
|
|
982
|
+
if (this.#clearedScoped.get(key)?.has(refusal.revokedCid)) return "scoped";
|
|
983
|
+
if (!Array.isArray(additional)) return void 0;
|
|
984
|
+
const affected = additional.some((entry) => {
|
|
985
|
+
const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
|
|
986
|
+
return typeof cid === "string" && (cid === refusal.revokedCid || cid === ancestorCid);
|
|
987
|
+
});
|
|
988
|
+
const inProgress = this.#scopedClearing.get(key);
|
|
989
|
+
if (inProgress) {
|
|
990
|
+
await inProgress;
|
|
991
|
+
return this.#clearRefusedAuthority(tenant, refusal);
|
|
992
|
+
}
|
|
993
|
+
if (!affected) return void 0;
|
|
994
|
+
const clearing = (async () => {
|
|
995
|
+
const latest = await readJson(this.#additionalPath(tenant.stateRoot));
|
|
996
|
+
if (!Array.isArray(latest)) return;
|
|
997
|
+
const remaining = latest.filter((entry) => {
|
|
998
|
+
const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
|
|
999
|
+
return typeof cid !== "string" || cid !== refusal.revokedCid && cid !== ancestorCid;
|
|
1000
|
+
});
|
|
1001
|
+
if (remaining.length === latest.length) return;
|
|
1002
|
+
this.#activeSessions.delete(tenant.stateRoot);
|
|
1003
|
+
const epoch = (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1;
|
|
1004
|
+
this.#approvalEpochs.set(tenant.stateRoot, epoch);
|
|
1005
|
+
const profilePath = this.#profilePath(tenant.stateRoot);
|
|
1006
|
+
const profile = await readJson(profilePath);
|
|
1007
|
+
await secureWrite(profilePath, { ...profile, approvalEpoch: epoch });
|
|
1008
|
+
if (remaining.length === 0) await (0, import_promises.rm)(this.#additionalPath(tenant.stateRoot), { force: true });
|
|
1009
|
+
else await secureWrite(this.#additionalPath(tenant.stateRoot), remaining);
|
|
1010
|
+
const cleared = removed ?? /* @__PURE__ */ new Set();
|
|
1011
|
+
cleared.add(refusal.revokedCid);
|
|
1012
|
+
this.#clearedScoped.set(tenant.stateRoot, cleared);
|
|
1013
|
+
})().finally(() => this.#scopedClearing.delete(key));
|
|
1014
|
+
this.#scopedClearing.set(key, clearing);
|
|
1015
|
+
await clearing;
|
|
1016
|
+
return "scoped";
|
|
1017
|
+
}
|
|
885
1018
|
async approvalRedirect(state) {
|
|
886
1019
|
const pending = await this.#readPending(state, false);
|
|
887
1020
|
const tenant = await this.#readTenant(pending.tenantId);
|
|
@@ -922,21 +1055,36 @@ var RemoteTenantStore = class {
|
|
|
922
1055
|
}
|
|
923
1056
|
if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
|
|
924
1057
|
const pending = await this.#readPending(state, true);
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
await this.#
|
|
1058
|
+
const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", pending.tenantId);
|
|
1059
|
+
const prior = this.#approvalProcessing.get(stateRoot);
|
|
1060
|
+
const processing = (async () => {
|
|
1061
|
+
if (prior) await prior.catch(() => void 0);
|
|
1062
|
+
try {
|
|
1063
|
+
const tenant = await this.#readTenant(pending.tenantId);
|
|
1064
|
+
if ((pending.approvalEpoch ?? 0) !== tenant.approvalEpoch) {
|
|
1065
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
1066
|
+
}
|
|
1067
|
+
if (pending.kind === "bootstrap") {
|
|
1068
|
+
await this.#storeBootstrap(tenant, pending, data);
|
|
1069
|
+
} else {
|
|
1070
|
+
await this.#storeDelegation(tenant, pending, data);
|
|
1071
|
+
}
|
|
1072
|
+
await (0, import_promises.rm)(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
await this.#restorePending(pending).catch(() => void 0);
|
|
1075
|
+
throw error;
|
|
931
1076
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1077
|
+
})();
|
|
1078
|
+
this.#approvalProcessing.set(stateRoot, processing);
|
|
1079
|
+
try {
|
|
1080
|
+
await processing;
|
|
1081
|
+
} finally {
|
|
1082
|
+
if (this.#approvalProcessing.get(stateRoot) === processing) this.#approvalProcessing.delete(stateRoot);
|
|
936
1083
|
}
|
|
937
1084
|
}
|
|
938
1085
|
async #storeBootstrap(tenant, pending, data) {
|
|
939
1086
|
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
1087
|
+
this.#assertCurrentApproval(pending, session);
|
|
940
1088
|
if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
|
|
941
1089
|
throw new ApprovalOwnerMismatchError();
|
|
942
1090
|
}
|
|
@@ -953,10 +1101,13 @@ var RemoteTenantStore = class {
|
|
|
953
1101
|
authMethod: "openkey",
|
|
954
1102
|
...session
|
|
955
1103
|
});
|
|
1104
|
+
this.#renewalRequired.delete(tenant.stateRoot);
|
|
1105
|
+
this.#activeSessions.delete(tenant.stateRoot);
|
|
956
1106
|
}
|
|
957
1107
|
async #storeDelegation(tenant, pending, data) {
|
|
958
1108
|
if (pending.requestId === void 0) throw new Error("Authority request is missing.");
|
|
959
1109
|
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
1110
|
+
this.#assertCurrentApproval(pending, session);
|
|
960
1111
|
if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
|
|
961
1112
|
let delegation;
|
|
962
1113
|
try {
|
|
@@ -1000,6 +1151,12 @@ var RemoteTenantStore = class {
|
|
|
1000
1151
|
}
|
|
1001
1152
|
if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
|
|
1002
1153
|
}
|
|
1154
|
+
#assertCurrentApproval(pending, session) {
|
|
1155
|
+
const issuedAt = /^Issued At: ([^\r\n]+)\r?$/m.exec(session.siwe)?.[1];
|
|
1156
|
+
if (issuedAt === void 0 || !Number.isFinite(Date.parse(issuedAt)) || Date.parse(issuedAt) < Date.parse(pending.createdAt)) {
|
|
1157
|
+
throw new ApprovalStageError("restore_approval", "APPROVAL_PREDATES_REQUEST");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1003
1160
|
/**
|
|
1004
1161
|
* Verify the OpenKey callback as a SIWE session for this tenant's key. The
|
|
1005
1162
|
* callback must carry the full signed proof (SIWE, signature, address and
|
|
@@ -1040,17 +1197,104 @@ var RemoteTenantStore = class {
|
|
|
1040
1197
|
this.#initializing.set(tenantId, initializing);
|
|
1041
1198
|
return initializing;
|
|
1042
1199
|
}
|
|
1043
|
-
async #hasValidSession(tenant, ownerDids) {
|
|
1044
|
-
|
|
1200
|
+
async #hasValidSession(tenant, ownerDids, fresh = false) {
|
|
1201
|
+
if (this.#renewalRequired.has(tenant.stateRoot)) return false;
|
|
1202
|
+
const key = JSON.stringify([tenant.stateRoot, ownerDids.map(normalizeDid), fresh]);
|
|
1203
|
+
const current = this.#sessionChecks.get(key);
|
|
1204
|
+
if (current) return current;
|
|
1205
|
+
const checking = this.#checkSession(tenant, ownerDids, fresh).finally(() => {
|
|
1206
|
+
this.#sessionChecks.delete(key);
|
|
1207
|
+
});
|
|
1208
|
+
this.#sessionChecks.set(key, checking);
|
|
1209
|
+
return checking;
|
|
1210
|
+
}
|
|
1211
|
+
async #checkSession(tenant, ownerDids, fresh) {
|
|
1212
|
+
let raw;
|
|
1213
|
+
try {
|
|
1214
|
+
raw = await readJson(this.#sessionPath(tenant.stateRoot));
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
1217
|
+
await this.#clearAuthority(tenant);
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1045
1220
|
if (raw === null) return false;
|
|
1221
|
+
const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
|
|
1046
1222
|
try {
|
|
1047
|
-
const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
|
|
1048
1223
|
await node.restoreSession(raw);
|
|
1049
|
-
return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
|
|
1050
1224
|
} catch {
|
|
1051
|
-
|
|
1225
|
+
const header = isRecord(raw.delegationHeader) ? raw.delegationHeader.Authorization : void 0;
|
|
1226
|
+
const signedExpiry = typeof header === "string" ? (0, import_node_sdk2.signedCompactUcanExpiry)(header) : void 0;
|
|
1227
|
+
const siweExpiry = typeof raw.siwe === "string" ? /^Expiration Time: ([^\r\n]+)\r?$/m.exec(raw.siwe)?.[1] : void 0;
|
|
1228
|
+
const expiry = signedExpiry ?? (siweExpiry === void 0 ? void 0 : new Date(siweExpiry));
|
|
1229
|
+
if (expiry !== void 0 && expiry.getTime() <= Date.now()) return false;
|
|
1230
|
+
await this.#clearAuthority(tenant);
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
if (!samePrincipal(node.sessionDid, tenant.sessionDid) || !ownerDids.some((did) => samePrincipal(node.did, did))) return false;
|
|
1234
|
+
const additional = await readJson(this.#additionalPath(tenant.stateRoot));
|
|
1235
|
+
const cids = [raw.delegationCid];
|
|
1236
|
+
if (additional !== null) {
|
|
1237
|
+
if (!Array.isArray(additional)) throw new Error("Stored TinyCloud authority is unavailable.");
|
|
1238
|
+
for (const entry of additional) {
|
|
1239
|
+
const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
|
|
1240
|
+
cids.push(cid);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (!cids.every((cid) => typeof cid === "string" && cid.length > 0)) {
|
|
1244
|
+
throw new Error("Stored TinyCloud authority is unavailable.");
|
|
1245
|
+
}
|
|
1246
|
+
const fingerprint = cids.join("\n");
|
|
1247
|
+
const cached = this.#activeSessions.get(tenant.stateRoot);
|
|
1248
|
+
if (!fresh && cached?.cids === fingerprint && cached.until > Date.now()) return true;
|
|
1249
|
+
let timer;
|
|
1250
|
+
const timeout = new Promise((resolve) => {
|
|
1251
|
+
timer = setTimeout(() => resolve(void 0), 2e3);
|
|
1252
|
+
timer.unref();
|
|
1253
|
+
});
|
|
1254
|
+
const probe = await Promise.race([
|
|
1255
|
+
node.kvForSpace(String(raw.spaceId)).list({ prefix: "spaces/" }).catch(() => void 0),
|
|
1256
|
+
timeout
|
|
1257
|
+
]).finally(() => clearTimeout(timer));
|
|
1258
|
+
const refusal = probe !== void 0 && !probe.ok ? (0, import_mcp_authority.nodeRevocationOf)(probe.error) : void 0;
|
|
1259
|
+
if (refusal !== void 0 && (refusal.revokedCid === raw.delegationCid || refusal.reason === "delegation_ancestor_revoked" && refusal.ancestorCid === raw.delegationCid)) {
|
|
1260
|
+
await this.#clearAuthority(tenant);
|
|
1052
1261
|
return false;
|
|
1053
1262
|
}
|
|
1263
|
+
if (this.#renewalRequired.has(tenant.stateRoot)) return false;
|
|
1264
|
+
if (probe?.ok) {
|
|
1265
|
+
this.#activeSessions.set(tenant.stateRoot, { cids: fingerprint, until: Date.now() + 3e4 });
|
|
1266
|
+
}
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
async #clearAuthority(tenant) {
|
|
1270
|
+
const current = this.#clearing.get(tenant.stateRoot);
|
|
1271
|
+
if (current) return current;
|
|
1272
|
+
this.#activeSessions.delete(tenant.stateRoot);
|
|
1273
|
+
this.#renewalRequired.add(tenant.stateRoot);
|
|
1274
|
+
this.#clearedScoped.delete(tenant.stateRoot);
|
|
1275
|
+
this.#approvalEpochs.set(
|
|
1276
|
+
tenant.stateRoot,
|
|
1277
|
+
(this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1
|
|
1278
|
+
);
|
|
1279
|
+
const clearing = (async () => {
|
|
1280
|
+
const processing = this.#approvalProcessing.get(tenant.stateRoot);
|
|
1281
|
+
if (processing) await processing.catch(() => void 0);
|
|
1282
|
+
const scoped = this.#scopedClearing.get(tenant.stateRoot);
|
|
1283
|
+
if (scoped) await scoped;
|
|
1284
|
+
this.#renewalRequired.add(tenant.stateRoot);
|
|
1285
|
+
const profilePath = this.#profilePath(tenant.stateRoot);
|
|
1286
|
+
const profile = await readJson(profilePath);
|
|
1287
|
+
await secureWrite(profilePath, {
|
|
1288
|
+
...profile,
|
|
1289
|
+
approvalEpoch: this.#approvalEpochs.get(tenant.stateRoot)
|
|
1290
|
+
});
|
|
1291
|
+
await (0, import_promises.rm)(this.#additionalPath(tenant.stateRoot), { force: true });
|
|
1292
|
+
await (0, import_promises.rm)((0, import_node_path3.join)(tenant.stateRoot, ".tinycloud", "profiles", PROFILE, "auth-requests.json"), { force: true });
|
|
1293
|
+
await (0, import_promises.rm)(this.#pendingDirectory(tenant.stateRoot), { recursive: true, force: true });
|
|
1294
|
+
await (0, import_promises.rm)(this.#sessionPath(tenant.stateRoot), { force: true });
|
|
1295
|
+
})().finally(() => this.#clearing.delete(tenant.stateRoot));
|
|
1296
|
+
this.#clearing.set(tenant.stateRoot, clearing);
|
|
1297
|
+
await clearing;
|
|
1054
1298
|
}
|
|
1055
1299
|
async #initializeTenant(tenantId) {
|
|
1056
1300
|
const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -1089,21 +1333,36 @@ var RemoteTenantStore = class {
|
|
|
1089
1333
|
`
|
|
1090
1334
|
);
|
|
1091
1335
|
}
|
|
1092
|
-
|
|
1336
|
+
const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
|
|
1337
|
+
this.#approvalEpochs.set(stateRoot, approvalEpoch);
|
|
1338
|
+
return { stateRoot, sessionDid, jwk, approvalEpoch };
|
|
1093
1339
|
}
|
|
1094
1340
|
async #readTenant(tenantId) {
|
|
1095
1341
|
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1096
1342
|
const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
|
|
1097
1343
|
const jwk = await readJson(this.#keyPath(stateRoot));
|
|
1098
1344
|
if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
|
|
1099
|
-
|
|
1345
|
+
const profile = await readJson(this.#profilePath(stateRoot));
|
|
1346
|
+
const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
|
|
1347
|
+
return { stateRoot, jwk, sessionDid: didFromJwk(jwk), approvalEpoch };
|
|
1100
1348
|
}
|
|
1101
1349
|
async #createPending(subject, ownerDids, kind, permissions, requestId) {
|
|
1350
|
+
const clearing = this.#clearing.get(this.tenantStateRoot(subject));
|
|
1351
|
+
if (clearing) await clearing;
|
|
1102
1352
|
const tenant = await this.#ensureTenant(subject);
|
|
1103
1353
|
const tenantId = this.#tenantId(subject);
|
|
1354
|
+
const normalizedOwners = ownerDids.map(normalizeDid);
|
|
1355
|
+
const key = JSON.stringify([tenantId, tenant.approvalEpoch, kind, requestId, permissions, normalizedOwners]);
|
|
1356
|
+
const current = this.#pendingWrites.get(key);
|
|
1357
|
+
if (current) return current;
|
|
1358
|
+
const writing = this.#writePending(tenant, tenantId, kind, permissions, normalizedOwners, requestId).finally(() => this.#pendingWrites.delete(key));
|
|
1359
|
+
this.#pendingWrites.set(key, writing);
|
|
1360
|
+
return writing;
|
|
1361
|
+
}
|
|
1362
|
+
async #writePending(tenant, tenantId, kind, permissions, ownerDids, requestId) {
|
|
1104
1363
|
const reusable = await reusablePending(
|
|
1105
1364
|
this.#pendingDirectory(tenant.stateRoot),
|
|
1106
|
-
{ tenantId, kind, requestId, permissions, ownerDids
|
|
1365
|
+
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
1107
1366
|
);
|
|
1108
1367
|
if (reusable !== void 0) return reusable;
|
|
1109
1368
|
const nonce = (0, import_node_crypto.randomBytes)(24).toString("base64url");
|
|
@@ -1112,10 +1371,11 @@ var RemoteTenantStore = class {
|
|
|
1112
1371
|
version: 1,
|
|
1113
1372
|
nonce,
|
|
1114
1373
|
tenantId,
|
|
1374
|
+
approvalEpoch: tenant.approvalEpoch,
|
|
1115
1375
|
kind,
|
|
1116
1376
|
...requestId === void 0 ? {} : { requestId },
|
|
1117
1377
|
permissions,
|
|
1118
|
-
ownerDids
|
|
1378
|
+
ownerDids,
|
|
1119
1379
|
createdAt: new Date(created).toISOString(),
|
|
1120
1380
|
expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
|
|
1121
1381
|
};
|
|
@@ -1156,6 +1416,11 @@ var RemoteTenantStore = class {
|
|
|
1156
1416
|
if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
|
|
1157
1417
|
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
1158
1418
|
}
|
|
1419
|
+
const profile = await readJson(this.#profilePath(stateRoot));
|
|
1420
|
+
const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
|
|
1421
|
+
if ((pending.approvalEpoch ?? 0) !== approvalEpoch) {
|
|
1422
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
1423
|
+
}
|
|
1159
1424
|
if (new Date(pending.expiresAt).getTime() <= Date.now()) {
|
|
1160
1425
|
await (0, import_promises.rm)(consume ? claimed : path, { force: true });
|
|
1161
1426
|
throw new ApprovalRequestError("state_expired", "Approval state has expired.");
|
|
@@ -1191,6 +1456,9 @@ var RemoteTenantStore = class {
|
|
|
1191
1456
|
#sessionPath(stateRoot) {
|
|
1192
1457
|
return (0, import_node_path3.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
|
|
1193
1458
|
}
|
|
1459
|
+
#additionalPath(stateRoot) {
|
|
1460
|
+
return (0, import_node_path3.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "additional-delegations.json");
|
|
1461
|
+
}
|
|
1194
1462
|
#pendingPath(stateRoot, nonce) {
|
|
1195
1463
|
return (0, import_node_path3.join)(this.#pendingDirectory(stateRoot), `${nonce}.json`);
|
|
1196
1464
|
}
|
|
@@ -1218,6 +1486,21 @@ function ensureWasm() {
|
|
|
1218
1486
|
(0, import_node_sdk_wasm.initPanicHook)();
|
|
1219
1487
|
wasmInitialized = true;
|
|
1220
1488
|
}
|
|
1489
|
+
function revocationFromResult(error) {
|
|
1490
|
+
const details = error.details;
|
|
1491
|
+
if (!isRecord(details) || typeof details.revokedCid !== "string") return void 0;
|
|
1492
|
+
if (details.reason === "delegation_revoked") {
|
|
1493
|
+
return { reason: "delegation_revoked", revokedCid: details.revokedCid };
|
|
1494
|
+
}
|
|
1495
|
+
if (details.reason === "delegation_ancestor_revoked" && typeof details.ancestorCid === "string") {
|
|
1496
|
+
return {
|
|
1497
|
+
reason: "delegation_ancestor_revoked",
|
|
1498
|
+
revokedCid: details.revokedCid,
|
|
1499
|
+
ancestorCid: details.ancestorCid
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
return void 0;
|
|
1503
|
+
}
|
|
1221
1504
|
function sessionFromCallback(data, privateJwk) {
|
|
1222
1505
|
const delegationHeader = data.delegationHeader;
|
|
1223
1506
|
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") {
|
|
@@ -1308,7 +1591,7 @@ async function reusablePending(directory, expected) {
|
|
|
1308
1591
|
return void 0;
|
|
1309
1592
|
}
|
|
1310
1593
|
function samePendingRequest(pending, expected) {
|
|
1311
|
-
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);
|
|
1594
|
+
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);
|
|
1312
1595
|
}
|
|
1313
1596
|
|
|
1314
1597
|
// src/http.ts
|
|
@@ -1348,7 +1631,8 @@ function createHostedMcpApp(config) {
|
|
|
1348
1631
|
explicitProfile: true,
|
|
1349
1632
|
allowOwnerProfile: false,
|
|
1350
1633
|
stateRoot,
|
|
1351
|
-
|
|
1634
|
+
beforeOperation: () => store.validateTenant(subject, ownerDids),
|
|
1635
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
1352
1636
|
}, validator);
|
|
1353
1637
|
registerConnectTool(server, validator, store, identity);
|
|
1354
1638
|
return server;
|