@tinycloud/mcp 0.7.0-beta.35 → 0.7.0-beta.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/cli.cjs +8 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +318 -34
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +320 -35
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +318 -34
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +320 -35
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +318 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +320 -35
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +8 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +8 -3
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +6 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +9 -2
- package/dist/tools.d.ts +9 -2
- package/dist/tools.js +6 -1
- package/dist/tools.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -403,6 +403,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
403
403
|
const metrics = { replayedGrants: 0 };
|
|
404
404
|
let code = "INTERNAL_ERROR";
|
|
405
405
|
try {
|
|
406
|
+
await selection.beforeOperation?.();
|
|
406
407
|
const result = await invokeOperation(
|
|
407
408
|
binding.operationId,
|
|
408
409
|
binding.operationVersion,
|
|
@@ -410,7 +411,11 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
410
411
|
input,
|
|
411
412
|
metrics
|
|
412
413
|
);
|
|
413
|
-
const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result
|
|
414
|
+
const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result, {
|
|
415
|
+
operationId: binding.operationId,
|
|
416
|
+
operationVersion: binding.operationVersion,
|
|
417
|
+
input
|
|
418
|
+
});
|
|
414
419
|
const response = toMcpToolResult(projected, TOOL_BINDINGS, {
|
|
415
420
|
surface: selection.surface,
|
|
416
421
|
readOnly: binding.readOnlyHint,
|
|
@@ -442,7 +447,7 @@ function outcomeCode(result) {
|
|
|
442
447
|
// package.json
|
|
443
448
|
var package_default = {
|
|
444
449
|
name: "@tinycloud/mcp",
|
|
445
|
-
version: "0.7.0-beta.
|
|
450
|
+
version: "0.7.0-beta.36",
|
|
446
451
|
description: "TinyCloud delegated operations MCP server",
|
|
447
452
|
keywords: [
|
|
448
453
|
"tinycloud",
|
|
@@ -515,7 +520,7 @@ var package_default = {
|
|
|
515
520
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
516
521
|
"@tinycloud/node-sdk": "3.3.0-beta.36",
|
|
517
522
|
"@tinycloud/node-sdk-wasm": "1.7.8-beta.0",
|
|
518
|
-
"@tinycloud/operations": "0.7.0-beta.
|
|
523
|
+
"@tinycloud/operations": "0.7.0-beta.35",
|
|
519
524
|
jose: "6.2.4"
|
|
520
525
|
},
|
|
521
526
|
devDependencies: {
|
|
@@ -756,10 +761,12 @@ import {
|
|
|
756
761
|
UnsupportedSessionRestoreError,
|
|
757
762
|
isCapabilitySubset,
|
|
758
763
|
principalDidEquals,
|
|
759
|
-
siweRuntimeDelegation
|
|
764
|
+
siweRuntimeDelegation,
|
|
765
|
+
signedCompactUcanExpiry
|
|
760
766
|
} from "@tinycloud/node-sdk";
|
|
761
767
|
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
762
768
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
769
|
+
import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
|
|
763
770
|
import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
|
|
764
771
|
var PROFILE = "agent";
|
|
765
772
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
@@ -807,6 +814,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
807
814
|
var RemoteTenantStore = class {
|
|
808
815
|
#config;
|
|
809
816
|
#initializing = /* @__PURE__ */ new Map();
|
|
817
|
+
#sessionChecks = /* @__PURE__ */ new Map();
|
|
818
|
+
#clearing = /* @__PURE__ */ new Map();
|
|
819
|
+
#activeSessions = /* @__PURE__ */ new Map();
|
|
820
|
+
#pendingWrites = /* @__PURE__ */ new Map();
|
|
821
|
+
#renewalRequired = /* @__PURE__ */ new Set();
|
|
822
|
+
#approvalEpochs = /* @__PURE__ */ new Map();
|
|
823
|
+
#scopedClearing = /* @__PURE__ */ new Map();
|
|
824
|
+
#clearedScoped = /* @__PURE__ */ new Map();
|
|
825
|
+
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
810
826
|
constructor(config) {
|
|
811
827
|
this.#config = config;
|
|
812
828
|
}
|
|
@@ -817,9 +833,13 @@ var RemoteTenantStore = class {
|
|
|
817
833
|
async initializeTenant(subject) {
|
|
818
834
|
return (await this.#ensureTenant(subject)).stateRoot;
|
|
819
835
|
}
|
|
836
|
+
/** Check stored node authority before an authenticated operation uses it. */
|
|
837
|
+
async validateTenant(subject, ownerDids) {
|
|
838
|
+
await this.#hasValidSession(await this.#ensureTenant(subject), ownerDids);
|
|
839
|
+
}
|
|
820
840
|
async connectStatus(subject, ownerDids = []) {
|
|
821
841
|
const tenant = await this.#ensureTenant(subject);
|
|
822
|
-
if (await this.#hasValidSession(tenant, ownerDids)) {
|
|
842
|
+
if (await this.#hasValidSession(tenant, ownerDids, true)) {
|
|
823
843
|
return { connected: true, sessionDid: tenant.sessionDid };
|
|
824
844
|
}
|
|
825
845
|
const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
|
|
@@ -830,24 +850,54 @@ var RemoteTenantStore = class {
|
|
|
830
850
|
expiresAt: pending.expiresAt
|
|
831
851
|
};
|
|
832
852
|
}
|
|
833
|
-
async decorateAuthorityResult(subject, ownerDids, result) {
|
|
853
|
+
async decorateAuthorityResult(subject, ownerDids, result, invocation) {
|
|
834
854
|
if (!isRecord(result)) return result;
|
|
835
855
|
if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
|
|
836
|
-
await this.#ensureTenant(subject);
|
|
856
|
+
const tenant2 = await this.#ensureTenant(subject);
|
|
857
|
+
const refusal = revocationFromResult(result.error);
|
|
858
|
+
const cleared = refusal === void 0 ? void 0 : await this.#clearRefusedAuthority(tenant2, refusal);
|
|
859
|
+
if (cleared === "scoped" && invocation !== void 0) {
|
|
860
|
+
return this.#renewScopedApproval(subject, ownerDids, tenant2, result, invocation);
|
|
861
|
+
}
|
|
862
|
+
const valid = await this.#hasValidSession(tenant2, ownerDids, true);
|
|
863
|
+
if (valid && result.error.code === "AUTH_REQUIRED") {
|
|
864
|
+
if (refusal === void 0) return result;
|
|
865
|
+
return {
|
|
866
|
+
...result,
|
|
867
|
+
error: { code: "AUTH_REQUIRED", message: "TinyCloud approval is required.", retryable: false }
|
|
868
|
+
};
|
|
869
|
+
}
|
|
837
870
|
const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
|
|
838
871
|
return {
|
|
839
|
-
|
|
872
|
+
status: "error",
|
|
873
|
+
operation: result.operation,
|
|
874
|
+
context: result.context,
|
|
840
875
|
error: {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
expiresAt: pending2.expiresAt
|
|
846
|
-
}
|
|
876
|
+
code: result.error.code === "SESSION_NOT_FOUND" && await readJson(this.#sessionPath(tenant2.stateRoot)) === null ? "SESSION_NOT_FOUND" : "AUTH_REQUIRED",
|
|
877
|
+
message: "A new TinyCloud approval is required.",
|
|
878
|
+
retryable: false,
|
|
879
|
+
details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
|
|
847
880
|
}
|
|
848
881
|
};
|
|
849
882
|
}
|
|
850
883
|
if (result.status !== "authority_required") return result;
|
|
884
|
+
const tenant = await this.#ensureTenant(subject);
|
|
885
|
+
if (this.#renewalRequired.has(tenant.stateRoot) || !await this.#hasValidSession(tenant, ownerDids, true)) {
|
|
886
|
+
if (this.#renewalRequired.has(tenant.stateRoot)) {
|
|
887
|
+
const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
|
|
888
|
+
return {
|
|
889
|
+
status: "error",
|
|
890
|
+
operation: result.operation,
|
|
891
|
+
context: result.context,
|
|
892
|
+
error: {
|
|
893
|
+
code: "AUTH_REQUIRED",
|
|
894
|
+
message: "A new TinyCloud approval is required.",
|
|
895
|
+
retryable: false,
|
|
896
|
+
details: { approvalUrl: this.#connectUrl(pending2), expiresAt: pending2.expiresAt }
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
851
901
|
const request = isRecord(result.request) ? result.request : void 0;
|
|
852
902
|
const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
|
|
853
903
|
const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
|
|
@@ -864,6 +914,90 @@ var RemoteTenantStore = class {
|
|
|
864
914
|
}
|
|
865
915
|
};
|
|
866
916
|
}
|
|
917
|
+
async #renewScopedApproval(subject, ownerDids, tenant, result, invocation) {
|
|
918
|
+
const planned = await invokeOperation2(
|
|
919
|
+
"tinycloud.auth.request",
|
|
920
|
+
1,
|
|
921
|
+
{ profile: PROFILE, stateRoot: tenant.stateRoot },
|
|
922
|
+
invocation
|
|
923
|
+
);
|
|
924
|
+
const output = planned.status === "ok" && isRecord(planned.output) ? planned.output : void 0;
|
|
925
|
+
const request = isRecord(output?.request) ? output.request : void 0;
|
|
926
|
+
const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
|
|
927
|
+
const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
|
|
928
|
+
const missing = Array.isArray(output?.missing) ? output.missing.filter(isPermissionEntry) : [];
|
|
929
|
+
if (requestId === void 0 || requested.length === 0 || missing.length === 0) {
|
|
930
|
+
return {
|
|
931
|
+
status: "error",
|
|
932
|
+
operation: result.operation,
|
|
933
|
+
context: result.context,
|
|
934
|
+
error: { code: "AUTH_REQUIRED", message: "A new TinyCloud approval is required.", retryable: false }
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
|
|
938
|
+
return {
|
|
939
|
+
status: "authority_required",
|
|
940
|
+
operation: result.operation,
|
|
941
|
+
context: result.context,
|
|
942
|
+
missing,
|
|
943
|
+
request,
|
|
944
|
+
approval: {
|
|
945
|
+
kind: "openkey",
|
|
946
|
+
requestId,
|
|
947
|
+
url: this.#connectUrl(pending),
|
|
948
|
+
fallback: tcAppsApprovalMessage(requested) ?? "Open the approval URL, then retry the same MCP tool."
|
|
949
|
+
},
|
|
950
|
+
retry: operationRetryDescriptor(invocation, invocation.input)
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
async #clearRefusedAuthority(tenant, refusal) {
|
|
954
|
+
const raw = await readJson(this.#sessionPath(tenant.stateRoot)).catch(() => null);
|
|
955
|
+
const bootstrapCid = raw?.delegationCid;
|
|
956
|
+
const ancestorCid = refusal.reason === "delegation_ancestor_revoked" ? refusal.ancestorCid : void 0;
|
|
957
|
+
if (typeof bootstrapCid === "string" && (bootstrapCid === refusal.revokedCid || bootstrapCid === ancestorCid)) {
|
|
958
|
+
await this.#clearAuthority(tenant);
|
|
959
|
+
return "bootstrap";
|
|
960
|
+
}
|
|
961
|
+
const key = tenant.stateRoot;
|
|
962
|
+
const removed = this.#clearedScoped.get(tenant.stateRoot);
|
|
963
|
+
if (removed?.has(refusal.revokedCid)) return "scoped";
|
|
964
|
+
const additional = await readJson(this.#additionalPath(tenant.stateRoot));
|
|
965
|
+
if (this.#clearedScoped.get(key)?.has(refusal.revokedCid)) return "scoped";
|
|
966
|
+
if (!Array.isArray(additional)) return void 0;
|
|
967
|
+
const affected = additional.some((entry) => {
|
|
968
|
+
const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
|
|
969
|
+
return typeof cid === "string" && (cid === refusal.revokedCid || cid === ancestorCid);
|
|
970
|
+
});
|
|
971
|
+
const inProgress = this.#scopedClearing.get(key);
|
|
972
|
+
if (inProgress) {
|
|
973
|
+
await inProgress;
|
|
974
|
+
return this.#clearRefusedAuthority(tenant, refusal);
|
|
975
|
+
}
|
|
976
|
+
if (!affected) return void 0;
|
|
977
|
+
const clearing = (async () => {
|
|
978
|
+
const latest = await readJson(this.#additionalPath(tenant.stateRoot));
|
|
979
|
+
if (!Array.isArray(latest)) return;
|
|
980
|
+
const remaining = latest.filter((entry) => {
|
|
981
|
+
const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
|
|
982
|
+
return typeof cid !== "string" || cid !== refusal.revokedCid && cid !== ancestorCid;
|
|
983
|
+
});
|
|
984
|
+
if (remaining.length === latest.length) return;
|
|
985
|
+
this.#activeSessions.delete(tenant.stateRoot);
|
|
986
|
+
const epoch = (this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1;
|
|
987
|
+
this.#approvalEpochs.set(tenant.stateRoot, epoch);
|
|
988
|
+
const profilePath = this.#profilePath(tenant.stateRoot);
|
|
989
|
+
const profile = await readJson(profilePath);
|
|
990
|
+
await secureWrite(profilePath, { ...profile, approvalEpoch: epoch });
|
|
991
|
+
if (remaining.length === 0) await rm(this.#additionalPath(tenant.stateRoot), { force: true });
|
|
992
|
+
else await secureWrite(this.#additionalPath(tenant.stateRoot), remaining);
|
|
993
|
+
const cleared = removed ?? /* @__PURE__ */ new Set();
|
|
994
|
+
cleared.add(refusal.revokedCid);
|
|
995
|
+
this.#clearedScoped.set(tenant.stateRoot, cleared);
|
|
996
|
+
})().finally(() => this.#scopedClearing.delete(key));
|
|
997
|
+
this.#scopedClearing.set(key, clearing);
|
|
998
|
+
await clearing;
|
|
999
|
+
return "scoped";
|
|
1000
|
+
}
|
|
867
1001
|
async approvalRedirect(state) {
|
|
868
1002
|
const pending = await this.#readPending(state, false);
|
|
869
1003
|
const tenant = await this.#readTenant(pending.tenantId);
|
|
@@ -904,21 +1038,36 @@ var RemoteTenantStore = class {
|
|
|
904
1038
|
}
|
|
905
1039
|
if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
|
|
906
1040
|
const pending = await this.#readPending(state, true);
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
await this.#
|
|
1041
|
+
const stateRoot = join2(this.#config.stateDir, "tenants", pending.tenantId);
|
|
1042
|
+
const prior = this.#approvalProcessing.get(stateRoot);
|
|
1043
|
+
const processing = (async () => {
|
|
1044
|
+
if (prior) await prior.catch(() => void 0);
|
|
1045
|
+
try {
|
|
1046
|
+
const tenant = await this.#readTenant(pending.tenantId);
|
|
1047
|
+
if ((pending.approvalEpoch ?? 0) !== tenant.approvalEpoch) {
|
|
1048
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
1049
|
+
}
|
|
1050
|
+
if (pending.kind === "bootstrap") {
|
|
1051
|
+
await this.#storeBootstrap(tenant, pending, data);
|
|
1052
|
+
} else {
|
|
1053
|
+
await this.#storeDelegation(tenant, pending, data);
|
|
1054
|
+
}
|
|
1055
|
+
await rm(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
await this.#restorePending(pending).catch(() => void 0);
|
|
1058
|
+
throw error;
|
|
913
1059
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1060
|
+
})();
|
|
1061
|
+
this.#approvalProcessing.set(stateRoot, processing);
|
|
1062
|
+
try {
|
|
1063
|
+
await processing;
|
|
1064
|
+
} finally {
|
|
1065
|
+
if (this.#approvalProcessing.get(stateRoot) === processing) this.#approvalProcessing.delete(stateRoot);
|
|
918
1066
|
}
|
|
919
1067
|
}
|
|
920
1068
|
async #storeBootstrap(tenant, pending, data) {
|
|
921
1069
|
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
1070
|
+
this.#assertCurrentApproval(pending, session);
|
|
922
1071
|
if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
|
|
923
1072
|
throw new ApprovalOwnerMismatchError();
|
|
924
1073
|
}
|
|
@@ -935,10 +1084,13 @@ var RemoteTenantStore = class {
|
|
|
935
1084
|
authMethod: "openkey",
|
|
936
1085
|
...session
|
|
937
1086
|
});
|
|
1087
|
+
this.#renewalRequired.delete(tenant.stateRoot);
|
|
1088
|
+
this.#activeSessions.delete(tenant.stateRoot);
|
|
938
1089
|
}
|
|
939
1090
|
async #storeDelegation(tenant, pending, data) {
|
|
940
1091
|
if (pending.requestId === void 0) throw new Error("Authority request is missing.");
|
|
941
1092
|
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
1093
|
+
this.#assertCurrentApproval(pending, session);
|
|
942
1094
|
if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
|
|
943
1095
|
let delegation;
|
|
944
1096
|
try {
|
|
@@ -982,6 +1134,12 @@ var RemoteTenantStore = class {
|
|
|
982
1134
|
}
|
|
983
1135
|
if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
|
|
984
1136
|
}
|
|
1137
|
+
#assertCurrentApproval(pending, session) {
|
|
1138
|
+
const issuedAt = /^Issued At: ([^\r\n]+)\r?$/m.exec(session.siwe)?.[1];
|
|
1139
|
+
if (issuedAt === void 0 || !Number.isFinite(Date.parse(issuedAt)) || Date.parse(issuedAt) < Date.parse(pending.createdAt)) {
|
|
1140
|
+
throw new ApprovalStageError("restore_approval", "APPROVAL_PREDATES_REQUEST");
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
985
1143
|
/**
|
|
986
1144
|
* Verify the OpenKey callback as a SIWE session for this tenant's key. The
|
|
987
1145
|
* callback must carry the full signed proof (SIWE, signature, address and
|
|
@@ -1022,17 +1180,104 @@ var RemoteTenantStore = class {
|
|
|
1022
1180
|
this.#initializing.set(tenantId, initializing);
|
|
1023
1181
|
return initializing;
|
|
1024
1182
|
}
|
|
1025
|
-
async #hasValidSession(tenant, ownerDids) {
|
|
1026
|
-
|
|
1183
|
+
async #hasValidSession(tenant, ownerDids, fresh = false) {
|
|
1184
|
+
if (this.#renewalRequired.has(tenant.stateRoot)) return false;
|
|
1185
|
+
const key = JSON.stringify([tenant.stateRoot, ownerDids.map(normalizeDid), fresh]);
|
|
1186
|
+
const current = this.#sessionChecks.get(key);
|
|
1187
|
+
if (current) return current;
|
|
1188
|
+
const checking = this.#checkSession(tenant, ownerDids, fresh).finally(() => {
|
|
1189
|
+
this.#sessionChecks.delete(key);
|
|
1190
|
+
});
|
|
1191
|
+
this.#sessionChecks.set(key, checking);
|
|
1192
|
+
return checking;
|
|
1193
|
+
}
|
|
1194
|
+
async #checkSession(tenant, ownerDids, fresh) {
|
|
1195
|
+
let raw;
|
|
1196
|
+
try {
|
|
1197
|
+
raw = await readJson(this.#sessionPath(tenant.stateRoot));
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
1200
|
+
await this.#clearAuthority(tenant);
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1027
1203
|
if (raw === null) return false;
|
|
1204
|
+
const node = new TinyCloudNode({ host: this.#config.nodeHost });
|
|
1028
1205
|
try {
|
|
1029
|
-
const node = new TinyCloudNode({ host: this.#config.nodeHost });
|
|
1030
1206
|
await node.restoreSession(raw);
|
|
1031
|
-
return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
|
|
1032
1207
|
} catch {
|
|
1033
|
-
|
|
1208
|
+
const header = isRecord(raw.delegationHeader) ? raw.delegationHeader.Authorization : void 0;
|
|
1209
|
+
const signedExpiry = typeof header === "string" ? signedCompactUcanExpiry(header) : void 0;
|
|
1210
|
+
const siweExpiry = typeof raw.siwe === "string" ? /^Expiration Time: ([^\r\n]+)\r?$/m.exec(raw.siwe)?.[1] : void 0;
|
|
1211
|
+
const expiry = signedExpiry ?? (siweExpiry === void 0 ? void 0 : new Date(siweExpiry));
|
|
1212
|
+
if (expiry !== void 0 && expiry.getTime() <= Date.now()) return false;
|
|
1213
|
+
await this.#clearAuthority(tenant);
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
if (!samePrincipal(node.sessionDid, tenant.sessionDid) || !ownerDids.some((did) => samePrincipal(node.did, did))) return false;
|
|
1217
|
+
const additional = await readJson(this.#additionalPath(tenant.stateRoot));
|
|
1218
|
+
const cids = [raw.delegationCid];
|
|
1219
|
+
if (additional !== null) {
|
|
1220
|
+
if (!Array.isArray(additional)) throw new Error("Stored TinyCloud authority is unavailable.");
|
|
1221
|
+
for (const entry of additional) {
|
|
1222
|
+
const cid = isRecord(entry) && isRecord(entry.delegation) ? entry.delegation.cid : void 0;
|
|
1223
|
+
cids.push(cid);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (!cids.every((cid) => typeof cid === "string" && cid.length > 0)) {
|
|
1227
|
+
throw new Error("Stored TinyCloud authority is unavailable.");
|
|
1228
|
+
}
|
|
1229
|
+
const fingerprint = cids.join("\n");
|
|
1230
|
+
const cached = this.#activeSessions.get(tenant.stateRoot);
|
|
1231
|
+
if (!fresh && cached?.cids === fingerprint && cached.until > Date.now()) return true;
|
|
1232
|
+
let timer;
|
|
1233
|
+
const timeout = new Promise((resolve) => {
|
|
1234
|
+
timer = setTimeout(() => resolve(void 0), 2e3);
|
|
1235
|
+
timer.unref();
|
|
1236
|
+
});
|
|
1237
|
+
const probe = await Promise.race([
|
|
1238
|
+
node.kvForSpace(String(raw.spaceId)).list({ prefix: "spaces/" }).catch(() => void 0),
|
|
1239
|
+
timeout
|
|
1240
|
+
]).finally(() => clearTimeout(timer));
|
|
1241
|
+
const refusal = probe !== void 0 && !probe.ok ? nodeRevocationOf(probe.error) : void 0;
|
|
1242
|
+
if (refusal !== void 0 && (refusal.revokedCid === raw.delegationCid || refusal.reason === "delegation_ancestor_revoked" && refusal.ancestorCid === raw.delegationCid)) {
|
|
1243
|
+
await this.#clearAuthority(tenant);
|
|
1034
1244
|
return false;
|
|
1035
1245
|
}
|
|
1246
|
+
if (this.#renewalRequired.has(tenant.stateRoot)) return false;
|
|
1247
|
+
if (probe?.ok) {
|
|
1248
|
+
this.#activeSessions.set(tenant.stateRoot, { cids: fingerprint, until: Date.now() + 3e4 });
|
|
1249
|
+
}
|
|
1250
|
+
return true;
|
|
1251
|
+
}
|
|
1252
|
+
async #clearAuthority(tenant) {
|
|
1253
|
+
const current = this.#clearing.get(tenant.stateRoot);
|
|
1254
|
+
if (current) return current;
|
|
1255
|
+
this.#activeSessions.delete(tenant.stateRoot);
|
|
1256
|
+
this.#renewalRequired.add(tenant.stateRoot);
|
|
1257
|
+
this.#clearedScoped.delete(tenant.stateRoot);
|
|
1258
|
+
this.#approvalEpochs.set(
|
|
1259
|
+
tenant.stateRoot,
|
|
1260
|
+
(this.#approvalEpochs.get(tenant.stateRoot) ?? tenant.approvalEpoch) + 1
|
|
1261
|
+
);
|
|
1262
|
+
const clearing = (async () => {
|
|
1263
|
+
const processing = this.#approvalProcessing.get(tenant.stateRoot);
|
|
1264
|
+
if (processing) await processing.catch(() => void 0);
|
|
1265
|
+
const scoped = this.#scopedClearing.get(tenant.stateRoot);
|
|
1266
|
+
if (scoped) await scoped;
|
|
1267
|
+
this.#renewalRequired.add(tenant.stateRoot);
|
|
1268
|
+
const profilePath = this.#profilePath(tenant.stateRoot);
|
|
1269
|
+
const profile = await readJson(profilePath);
|
|
1270
|
+
await secureWrite(profilePath, {
|
|
1271
|
+
...profile,
|
|
1272
|
+
approvalEpoch: this.#approvalEpochs.get(tenant.stateRoot)
|
|
1273
|
+
});
|
|
1274
|
+
await rm(this.#additionalPath(tenant.stateRoot), { force: true });
|
|
1275
|
+
await rm(join2(tenant.stateRoot, ".tinycloud", "profiles", PROFILE, "auth-requests.json"), { force: true });
|
|
1276
|
+
await rm(this.#pendingDirectory(tenant.stateRoot), { recursive: true, force: true });
|
|
1277
|
+
await rm(this.#sessionPath(tenant.stateRoot), { force: true });
|
|
1278
|
+
})().finally(() => this.#clearing.delete(tenant.stateRoot));
|
|
1279
|
+
this.#clearing.set(tenant.stateRoot, clearing);
|
|
1280
|
+
await clearing;
|
|
1036
1281
|
}
|
|
1037
1282
|
async #initializeTenant(tenantId) {
|
|
1038
1283
|
const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -1071,21 +1316,36 @@ var RemoteTenantStore = class {
|
|
|
1071
1316
|
`
|
|
1072
1317
|
);
|
|
1073
1318
|
}
|
|
1074
|
-
|
|
1319
|
+
const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
|
|
1320
|
+
this.#approvalEpochs.set(stateRoot, approvalEpoch);
|
|
1321
|
+
return { stateRoot, sessionDid, jwk, approvalEpoch };
|
|
1075
1322
|
}
|
|
1076
1323
|
async #readTenant(tenantId) {
|
|
1077
1324
|
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1078
1325
|
const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
|
|
1079
1326
|
const jwk = await readJson(this.#keyPath(stateRoot));
|
|
1080
1327
|
if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
|
|
1081
|
-
|
|
1328
|
+
const profile = await readJson(this.#profilePath(stateRoot));
|
|
1329
|
+
const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
|
|
1330
|
+
return { stateRoot, jwk, sessionDid: didFromJwk(jwk), approvalEpoch };
|
|
1082
1331
|
}
|
|
1083
1332
|
async #createPending(subject, ownerDids, kind, permissions, requestId) {
|
|
1333
|
+
const clearing = this.#clearing.get(this.tenantStateRoot(subject));
|
|
1334
|
+
if (clearing) await clearing;
|
|
1084
1335
|
const tenant = await this.#ensureTenant(subject);
|
|
1085
1336
|
const tenantId = this.#tenantId(subject);
|
|
1337
|
+
const normalizedOwners = ownerDids.map(normalizeDid);
|
|
1338
|
+
const key = JSON.stringify([tenantId, tenant.approvalEpoch, kind, requestId, permissions, normalizedOwners]);
|
|
1339
|
+
const current = this.#pendingWrites.get(key);
|
|
1340
|
+
if (current) return current;
|
|
1341
|
+
const writing = this.#writePending(tenant, tenantId, kind, permissions, normalizedOwners, requestId).finally(() => this.#pendingWrites.delete(key));
|
|
1342
|
+
this.#pendingWrites.set(key, writing);
|
|
1343
|
+
return writing;
|
|
1344
|
+
}
|
|
1345
|
+
async #writePending(tenant, tenantId, kind, permissions, ownerDids, requestId) {
|
|
1086
1346
|
const reusable = await reusablePending(
|
|
1087
1347
|
this.#pendingDirectory(tenant.stateRoot),
|
|
1088
|
-
{ tenantId, kind, requestId, permissions, ownerDids
|
|
1348
|
+
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
1089
1349
|
);
|
|
1090
1350
|
if (reusable !== void 0) return reusable;
|
|
1091
1351
|
const nonce = randomBytes(24).toString("base64url");
|
|
@@ -1094,10 +1354,11 @@ var RemoteTenantStore = class {
|
|
|
1094
1354
|
version: 1,
|
|
1095
1355
|
nonce,
|
|
1096
1356
|
tenantId,
|
|
1357
|
+
approvalEpoch: tenant.approvalEpoch,
|
|
1097
1358
|
kind,
|
|
1098
1359
|
...requestId === void 0 ? {} : { requestId },
|
|
1099
1360
|
permissions,
|
|
1100
|
-
ownerDids
|
|
1361
|
+
ownerDids,
|
|
1101
1362
|
createdAt: new Date(created).toISOString(),
|
|
1102
1363
|
expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
|
|
1103
1364
|
};
|
|
@@ -1138,6 +1399,11 @@ var RemoteTenantStore = class {
|
|
|
1138
1399
|
if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
|
|
1139
1400
|
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
1140
1401
|
}
|
|
1402
|
+
const profile = await readJson(this.#profilePath(stateRoot));
|
|
1403
|
+
const approvalEpoch = this.#approvalEpochs.get(stateRoot) ?? (typeof profile?.approvalEpoch === "number" ? profile.approvalEpoch : 0);
|
|
1404
|
+
if ((pending.approvalEpoch ?? 0) !== approvalEpoch) {
|
|
1405
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
1406
|
+
}
|
|
1141
1407
|
if (new Date(pending.expiresAt).getTime() <= Date.now()) {
|
|
1142
1408
|
await rm(consume ? claimed : path, { force: true });
|
|
1143
1409
|
throw new ApprovalRequestError("state_expired", "Approval state has expired.");
|
|
@@ -1173,6 +1439,9 @@ var RemoteTenantStore = class {
|
|
|
1173
1439
|
#sessionPath(stateRoot) {
|
|
1174
1440
|
return join2(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
|
|
1175
1441
|
}
|
|
1442
|
+
#additionalPath(stateRoot) {
|
|
1443
|
+
return join2(stateRoot, ".tinycloud", "profiles", PROFILE, "additional-delegations.json");
|
|
1444
|
+
}
|
|
1176
1445
|
#pendingPath(stateRoot, nonce) {
|
|
1177
1446
|
return join2(this.#pendingDirectory(stateRoot), `${nonce}.json`);
|
|
1178
1447
|
}
|
|
@@ -1200,6 +1469,21 @@ function ensureWasm() {
|
|
|
1200
1469
|
initPanicHook();
|
|
1201
1470
|
wasmInitialized = true;
|
|
1202
1471
|
}
|
|
1472
|
+
function revocationFromResult(error) {
|
|
1473
|
+
const details = error.details;
|
|
1474
|
+
if (!isRecord(details) || typeof details.revokedCid !== "string") return void 0;
|
|
1475
|
+
if (details.reason === "delegation_revoked") {
|
|
1476
|
+
return { reason: "delegation_revoked", revokedCid: details.revokedCid };
|
|
1477
|
+
}
|
|
1478
|
+
if (details.reason === "delegation_ancestor_revoked" && typeof details.ancestorCid === "string") {
|
|
1479
|
+
return {
|
|
1480
|
+
reason: "delegation_ancestor_revoked",
|
|
1481
|
+
revokedCid: details.revokedCid,
|
|
1482
|
+
ancestorCid: details.ancestorCid
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
return void 0;
|
|
1486
|
+
}
|
|
1203
1487
|
function sessionFromCallback(data, privateJwk) {
|
|
1204
1488
|
const delegationHeader = data.delegationHeader;
|
|
1205
1489
|
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") {
|
|
@@ -1290,7 +1574,7 @@ async function reusablePending(directory, expected) {
|
|
|
1290
1574
|
return void 0;
|
|
1291
1575
|
}
|
|
1292
1576
|
function samePendingRequest(pending, expected) {
|
|
1293
|
-
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);
|
|
1577
|
+
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);
|
|
1294
1578
|
}
|
|
1295
1579
|
|
|
1296
1580
|
// src/http.ts
|
|
@@ -1330,7 +1614,8 @@ function createHostedMcpApp(config) {
|
|
|
1330
1614
|
explicitProfile: true,
|
|
1331
1615
|
allowOwnerProfile: false,
|
|
1332
1616
|
stateRoot,
|
|
1333
|
-
|
|
1617
|
+
beforeOperation: () => store.validateTenant(subject, ownerDids),
|
|
1618
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
1334
1619
|
}, validator);
|
|
1335
1620
|
registerConnectTool(server, validator, store, identity);
|
|
1336
1621
|
return server;
|