@kya-os/mcp-i-core 1.2.3-canary.7 → 1.3.0
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/.claude/settings.local.json +9 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test$colon$coverage.log +4514 -0
- package/.turbo/turbo-test.log +2973 -0
- package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
- package/Composer 3.md +615 -0
- package/GPT-5.md +1169 -0
- package/OPUS-plan.md +352 -0
- package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
- package/PHASE_3_SUMMARY.md +317 -0
- package/PHASE_4.1.3_SUMMARY.md +428 -0
- package/PHASE_4.1_COMPLETE.md +525 -0
- package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
- package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
- package/TEST_PLAN.md +571 -0
- package/coverage/coverage-final.json +57 -0
- package/dist/__tests__/utils/mock-providers.d.ts +1 -2
- package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
- package/dist/__tests__/utils/mock-providers.js.map +1 -1
- package/dist/cache/oauth-config-cache.d.ts +69 -0
- package/dist/cache/oauth-config-cache.d.ts.map +1 -0
- package/dist/cache/oauth-config-cache.js +76 -0
- package/dist/cache/oauth-config-cache.js.map +1 -0
- package/dist/identity/idp-token-resolver.d.ts +53 -0
- package/dist/identity/idp-token-resolver.d.ts.map +1 -0
- package/dist/identity/idp-token-resolver.js +108 -0
- package/dist/identity/idp-token-resolver.js.map +1 -0
- package/dist/identity/idp-token-storage.interface.d.ts +42 -0
- package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
- package/dist/identity/idp-token-storage.interface.js +12 -0
- package/dist/identity/idp-token-storage.interface.js.map +1 -0
- package/dist/identity/user-did-manager.d.ts +39 -1
- package/dist/identity/user-did-manager.d.ts.map +1 -1
- package/dist/identity/user-did-manager.js +69 -3
- package/dist/identity/user-did-manager.js.map +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/audit-logger.d.ts +37 -0
- package/dist/runtime/audit-logger.d.ts.map +1 -0
- package/dist/runtime/audit-logger.js +9 -0
- package/dist/runtime/audit-logger.js.map +1 -0
- package/dist/runtime/base.d.ts +58 -2
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +266 -11
- package/dist/runtime/base.js.map +1 -1
- package/dist/services/access-control.service.d.ts.map +1 -1
- package/dist/services/access-control.service.js +200 -35
- package/dist/services/access-control.service.js.map +1 -1
- package/dist/services/authorization/authorization-registry.d.ts +29 -0
- package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
- package/dist/services/authorization/authorization-registry.js +57 -0
- package/dist/services/authorization/authorization-registry.js.map +1 -0
- package/dist/services/authorization/types.d.ts +53 -0
- package/dist/services/authorization/types.d.ts.map +1 -0
- package/dist/services/authorization/types.js +10 -0
- package/dist/services/authorization/types.js.map +1 -0
- package/dist/services/batch-delegation.service.d.ts +53 -0
- package/dist/services/batch-delegation.service.d.ts.map +1 -0
- package/dist/services/batch-delegation.service.js +95 -0
- package/dist/services/batch-delegation.service.js.map +1 -0
- package/dist/services/oauth-config.service.d.ts +53 -0
- package/dist/services/oauth-config.service.d.ts.map +1 -0
- package/dist/services/oauth-config.service.js +117 -0
- package/dist/services/oauth-config.service.js.map +1 -0
- package/dist/services/oauth-provider-registry.d.ts +77 -0
- package/dist/services/oauth-provider-registry.d.ts.map +1 -0
- package/dist/services/oauth-provider-registry.js +112 -0
- package/dist/services/oauth-provider-registry.js.map +1 -0
- package/dist/services/oauth-service.d.ts +77 -0
- package/dist/services/oauth-service.d.ts.map +1 -0
- package/dist/services/oauth-service.js +348 -0
- package/dist/services/oauth-service.js.map +1 -0
- package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
- package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
- package/dist/services/oauth-token-retrieval.service.js +150 -0
- package/dist/services/oauth-token-retrieval.service.js.map +1 -0
- package/dist/services/provider-resolver.d.ts +48 -0
- package/dist/services/provider-resolver.d.ts.map +1 -0
- package/dist/services/provider-resolver.js +120 -0
- package/dist/services/provider-resolver.js.map +1 -0
- package/dist/services/provider-validator.d.ts +55 -0
- package/dist/services/provider-validator.d.ts.map +1 -0
- package/dist/services/provider-validator.js +135 -0
- package/dist/services/provider-validator.js.map +1 -0
- package/dist/services/tool-context-builder.d.ts +57 -0
- package/dist/services/tool-context-builder.d.ts.map +1 -0
- package/dist/services/tool-context-builder.js +125 -0
- package/dist/services/tool-context-builder.js.map +1 -0
- package/dist/services/tool-protection.service.d.ts +87 -10
- package/dist/services/tool-protection.service.d.ts.map +1 -1
- package/dist/services/tool-protection.service.js +282 -112
- package/dist/services/tool-protection.service.js.map +1 -1
- package/dist/types/oauth-required-error.d.ts +40 -0
- package/dist/types/oauth-required-error.d.ts.map +1 -0
- package/dist/types/oauth-required-error.js +40 -0
- package/dist/types/oauth-required-error.js.map +1 -0
- package/dist/utils/did-helpers.d.ts +33 -0
- package/dist/utils/did-helpers.d.ts.map +1 -1
- package/dist/utils/did-helpers.js +40 -0
- package/dist/utils/did-helpers.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/docs/API_REFERENCE.md +1362 -0
- package/docs/COMPLIANCE_MATRIX.md +691 -0
- package/docs/STATUSLIST2021_GUIDE.md +696 -0
- package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
- package/package.json +24 -50
- package/scripts/audit-compliance.ts +724 -0
- package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
- package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
- package/src/__tests__/delegation-e2e.test.ts +690 -0
- package/src/__tests__/identity/user-did-manager.test.ts +213 -0
- package/src/__tests__/index.test.ts +56 -0
- package/src/__tests__/integration/full-flow.test.ts +776 -0
- package/src/__tests__/integration.test.ts +281 -0
- package/src/__tests__/providers/base.test.ts +173 -0
- package/src/__tests__/providers/memory.test.ts +319 -0
- package/src/__tests__/regression/phase2-regression.test.ts +427 -0
- package/src/__tests__/runtime/audit-logger.test.ts +154 -0
- package/src/__tests__/runtime/base-extensions.test.ts +593 -0
- package/src/__tests__/runtime/base.test.ts +869 -0
- package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
- package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
- package/src/__tests__/runtime/route-interception.test.ts +686 -0
- package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
- package/src/__tests__/services/agentshield-integration.test.ts +784 -0
- package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
- package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
- package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
- package/src/__tests__/utils/mock-providers.ts +340 -0
- package/src/cache/oauth-config-cache.d.ts +69 -0
- package/src/cache/oauth-config-cache.d.ts.map +1 -0
- package/src/cache/oauth-config-cache.js +71 -0
- package/src/cache/oauth-config-cache.js.map +1 -0
- package/src/cache/oauth-config-cache.ts +123 -0
- package/src/cache/tool-protection-cache.ts +171 -0
- package/src/compliance/EXAMPLE.md +412 -0
- package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
- package/src/compliance/index.ts +8 -0
- package/src/compliance/schema-registry.ts +460 -0
- package/src/compliance/schema-verifier.ts +708 -0
- package/src/config/__tests__/remote-config.spec.ts +268 -0
- package/src/config/remote-config.ts +174 -0
- package/src/config.ts +309 -0
- package/src/delegation/__tests__/audience-validator.test.ts +112 -0
- package/src/delegation/__tests__/bitstring.test.ts +346 -0
- package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
- package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
- package/src/delegation/__tests__/utils.test.ts +152 -0
- package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
- package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
- package/src/delegation/audience-validator.ts +52 -0
- package/src/delegation/bitstring.ts +278 -0
- package/src/delegation/cascading-revocation.ts +370 -0
- package/src/delegation/delegation-graph.ts +299 -0
- package/src/delegation/index.ts +14 -0
- package/src/delegation/statuslist-manager.ts +353 -0
- package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
- package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
- package/src/delegation/storage/index.ts +9 -0
- package/src/delegation/storage/memory-graph-storage.ts +178 -0
- package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
- package/src/delegation/utils.ts +42 -0
- package/src/delegation/vc-issuer.ts +232 -0
- package/src/delegation/vc-verifier.ts +568 -0
- package/src/identity/idp-token-resolver.ts +147 -0
- package/src/identity/idp-token-storage.interface.ts +59 -0
- package/src/identity/user-did-manager.ts +370 -0
- package/src/index.ts +260 -0
- package/src/providers/base.d.ts +91 -0
- package/src/providers/base.d.ts.map +1 -0
- package/src/providers/base.js +38 -0
- package/src/providers/base.js.map +1 -0
- package/src/providers/base.ts +96 -0
- package/src/providers/memory.ts +142 -0
- package/src/runtime/audit-logger.ts +39 -0
- package/src/runtime/base.ts +1329 -0
- package/src/services/__tests__/access-control.integration.test.ts +443 -0
- package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
- package/src/services/__tests__/access-control.service.test.ts +970 -0
- package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
- package/src/services/__tests__/crypto.service.test.ts +531 -0
- package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
- package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
- package/src/services/__tests__/proof-verifier.test.ts +489 -0
- package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
- package/src/services/__tests__/provider-resolver.test.ts +217 -0
- package/src/services/__tests__/storage.service.test.ts +358 -0
- package/src/services/access-control.service.ts +990 -0
- package/src/services/authorization/authorization-registry.ts +66 -0
- package/src/services/authorization/types.ts +71 -0
- package/src/services/batch-delegation.service.ts +137 -0
- package/src/services/crypto.service.ts +302 -0
- package/src/services/errors.ts +76 -0
- package/src/services/index.ts +9 -0
- package/src/services/oauth-config.service.d.ts +53 -0
- package/src/services/oauth-config.service.d.ts.map +1 -0
- package/src/services/oauth-config.service.js +113 -0
- package/src/services/oauth-config.service.js.map +1 -0
- package/src/services/oauth-config.service.ts +166 -0
- package/src/services/oauth-provider-registry.d.ts +57 -0
- package/src/services/oauth-provider-registry.d.ts.map +1 -0
- package/src/services/oauth-provider-registry.js +73 -0
- package/src/services/oauth-provider-registry.js.map +1 -0
- package/src/services/oauth-provider-registry.ts +123 -0
- package/src/services/oauth-service.ts +510 -0
- package/src/services/oauth-token-retrieval.service.ts +245 -0
- package/src/services/proof-verifier.ts +478 -0
- package/src/services/provider-resolver.d.ts +48 -0
- package/src/services/provider-resolver.d.ts.map +1 -0
- package/src/services/provider-resolver.js +106 -0
- package/src/services/provider-resolver.js.map +1 -0
- package/src/services/provider-resolver.ts +144 -0
- package/src/services/provider-validator.ts +170 -0
- package/src/services/storage.service.ts +566 -0
- package/src/services/tool-context-builder.ts +172 -0
- package/src/services/tool-protection.service.ts +958 -0
- package/src/types/oauth-required-error.ts +63 -0
- package/src/types/tool-protection.ts +155 -0
- package/src/utils/__tests__/did-helpers.test.ts +101 -0
- package/src/utils/base64.ts +148 -0
- package/src/utils/cors.ts +83 -0
- package/src/utils/did-helpers.ts +150 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/storage-keys.ts +278 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +56 -0
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCPIRuntimeBase - Provider-based runtime
|
|
3
|
+
*
|
|
4
|
+
* Core runtime that accepts providers for all platform-specific operations.
|
|
5
|
+
* This enables the same runtime logic to work across Node.js, Cloudflare Workers,
|
|
6
|
+
* and other platforms.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CryptoProvider,
|
|
11
|
+
ClockProvider,
|
|
12
|
+
FetchProvider,
|
|
13
|
+
StorageProvider,
|
|
14
|
+
NonceCacheProvider,
|
|
15
|
+
IdentityProvider,
|
|
16
|
+
AgentIdentity,
|
|
17
|
+
} from "../providers/base";
|
|
18
|
+
import { DelegationRequiredError } from "../types/tool-protection.js";
|
|
19
|
+
import { CryptoService, type Ed25519JWK } from "../services/crypto.service.js";
|
|
20
|
+
import { ProofVerifier } from "../services/proof-verifier.js";
|
|
21
|
+
import type { DetachedProof } from "@kya-os/contracts/proof";
|
|
22
|
+
import type {
|
|
23
|
+
DIDDocument,
|
|
24
|
+
AgentDocument,
|
|
25
|
+
MCPIdentity,
|
|
26
|
+
WellKnownConfig,
|
|
27
|
+
WellKnownResponse,
|
|
28
|
+
} from "@kya-os/contracts/well-known";
|
|
29
|
+
import type { AccessControlApiService } from "../services/access-control.service.js";
|
|
30
|
+
import type { VerifyDelegationRequest } from "@kya-os/contracts/agentshield-api";
|
|
31
|
+
import { AgentShieldAPIError } from "@kya-os/contracts/agentshield-api";
|
|
32
|
+
|
|
33
|
+
// Import the new provider runtime config
|
|
34
|
+
import type { ProviderRuntimeConfig } from "../config";
|
|
35
|
+
import { UserDidManager } from "../identity/user-did-manager";
|
|
36
|
+
import type { InterceptedToolCall } from "../types/tool-protection.js";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Interface for runtime instances that have AccessControlApiService available
|
|
40
|
+
* This allows type-safe access to the access control service without using `as any`
|
|
41
|
+
*
|
|
42
|
+
* @deprecated AccessControlApiService is now directly available as protected property on MCPIRuntimeBase
|
|
43
|
+
*/
|
|
44
|
+
export interface RuntimeWithAccessControl {
|
|
45
|
+
accessControlService?: AccessControlApiService;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class MCPIRuntimeBase {
|
|
49
|
+
protected crypto: CryptoProvider;
|
|
50
|
+
protected clock: ClockProvider;
|
|
51
|
+
protected fetch: FetchProvider;
|
|
52
|
+
protected storage: StorageProvider;
|
|
53
|
+
protected nonceCache: NonceCacheProvider;
|
|
54
|
+
protected identity: IdentityProvider;
|
|
55
|
+
protected config: ProviderRuntimeConfig;
|
|
56
|
+
private cachedIdentity?: AgentIdentity;
|
|
57
|
+
private sessions: Map<string, any> = new Map();
|
|
58
|
+
private lastProof?: any;
|
|
59
|
+
private userDidManager?: UserDidManager;
|
|
60
|
+
private interceptedCalls: Map<string, any> = new Map(); // Store intercepted tool calls by resume token
|
|
61
|
+
private cryptoService?: CryptoService;
|
|
62
|
+
protected proofVerifier?: ProofVerifier; // Optional ProofVerifier (injected by subclasses)
|
|
63
|
+
protected accessControlService?: AccessControlApiService; // Optional AccessControlApiService (injected by subclasses)
|
|
64
|
+
|
|
65
|
+
constructor(config: ProviderRuntimeConfig) {
|
|
66
|
+
this.config = config;
|
|
67
|
+
this.crypto = config.cryptoProvider;
|
|
68
|
+
this.clock = config.clockProvider;
|
|
69
|
+
this.fetch = config.fetchProvider;
|
|
70
|
+
this.storage = config.storageProvider;
|
|
71
|
+
this.nonceCache = config.nonceCacheProvider;
|
|
72
|
+
this.identity = config.identityProvider;
|
|
73
|
+
// Initialize CryptoService for JWS verification
|
|
74
|
+
this.cryptoService = new CryptoService(this.crypto);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initialize the runtime
|
|
79
|
+
*/
|
|
80
|
+
async initialize(): Promise<void> {
|
|
81
|
+
// Load or generate identity
|
|
82
|
+
this.cachedIdentity = await this.identity.getIdentity();
|
|
83
|
+
|
|
84
|
+
// Initialize user DID manager if identity config enables it
|
|
85
|
+
if (this.config.identity?.generateUserDids) {
|
|
86
|
+
this.userDidManager = new UserDidManager({
|
|
87
|
+
crypto: this.crypto,
|
|
88
|
+
storage: this.config.identity?.userDidStorage
|
|
89
|
+
? {
|
|
90
|
+
get: async (key: string) =>
|
|
91
|
+
await this.storage.get(`userDid:${key}`),
|
|
92
|
+
set: async (key: string, value: string, ttl?: number) => {
|
|
93
|
+
await this.storage.set(`userDid:${key}`, value);
|
|
94
|
+
},
|
|
95
|
+
delete: async (key: string) =>
|
|
96
|
+
await this.storage.delete(`userDid:${key}`),
|
|
97
|
+
}
|
|
98
|
+
: undefined,
|
|
99
|
+
useDidWeb: this.config.identity?.userDidStorage === "persistent",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Initialize nonce cache if it has an initialize method
|
|
104
|
+
if (
|
|
105
|
+
"initialize" in this.nonceCache &&
|
|
106
|
+
typeof (this.nonceCache as any).initialize === "function"
|
|
107
|
+
) {
|
|
108
|
+
await (this.nonceCache as any).initialize();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Log initialization if audit is enabled
|
|
112
|
+
if (this.config.audit?.enabled) {
|
|
113
|
+
this.logAudit("runtime_initialized", {
|
|
114
|
+
did: this.cachedIdentity.did,
|
|
115
|
+
environment: this.config.environment || "development",
|
|
116
|
+
userDidGeneration: this.config.identity?.generateUserDids
|
|
117
|
+
? "enabled"
|
|
118
|
+
: "disabled",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get the current agent identity
|
|
125
|
+
*/
|
|
126
|
+
async getIdentity(): Promise<AgentIdentity> {
|
|
127
|
+
if (!this.cachedIdentity) {
|
|
128
|
+
this.cachedIdentity = await this.identity.getIdentity();
|
|
129
|
+
}
|
|
130
|
+
return this.cachedIdentity;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Handle handshake request
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Handle MCP handshake request
|
|
138
|
+
*
|
|
139
|
+
* @param request - Handshake request object (may include oauthIdentity for persistent user DID lookup)
|
|
140
|
+
* @returns Handshake response with session ID and agent DID
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* - Accepts optional oauthIdentity via request.oauthIdentity (backward compatible)
|
|
144
|
+
* - If OAuth identity provided, uses it to retrieve/create persistent user DID
|
|
145
|
+
* - Falls back to ephemeral user DID generation if OAuth unavailable
|
|
146
|
+
*/
|
|
147
|
+
async handleHandshake(
|
|
148
|
+
request: any & {
|
|
149
|
+
oauthIdentity?:
|
|
150
|
+
| import("../identity/user-did-manager").OAuthIdentity
|
|
151
|
+
| null;
|
|
152
|
+
}
|
|
153
|
+
): Promise<any> {
|
|
154
|
+
const identity = await this.getIdentity();
|
|
155
|
+
const timestamp = this.clock.now();
|
|
156
|
+
const sessionId = await this.generateSessionId();
|
|
157
|
+
|
|
158
|
+
// Generate user DID if user DID generation is enabled
|
|
159
|
+
// Use OAuth identity if provided for persistent user DID lookup
|
|
160
|
+
let userDid: string | undefined;
|
|
161
|
+
if (this.userDidManager) {
|
|
162
|
+
try {
|
|
163
|
+
const oauthIdentity = request.oauthIdentity;
|
|
164
|
+
userDid = await this.userDidManager.getOrCreateUserDid(
|
|
165
|
+
sessionId,
|
|
166
|
+
oauthIdentity
|
|
167
|
+
);
|
|
168
|
+
if (this.config.audit?.enabled) {
|
|
169
|
+
console.log("[MCP-I] Generated user DID for session:", {
|
|
170
|
+
userDid: userDid.substring(0, 20) + "...",
|
|
171
|
+
hasOAuth: !!oauthIdentity,
|
|
172
|
+
provider: oauthIdentity?.provider,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn("[MCP-I] Failed to generate user DID:", error);
|
|
177
|
+
// Continue without user DID - not critical
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Extract client info if available
|
|
182
|
+
const normalizeString = (value: unknown): string | undefined => {
|
|
183
|
+
if (typeof value !== "string") {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const trimmed = value.trim();
|
|
187
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const protocolVersion = normalizeString(request.clientProtocolVersion);
|
|
191
|
+
const clientCapabilities = request.clientCapabilities;
|
|
192
|
+
|
|
193
|
+
const requestClientInfo = request.clientInfo;
|
|
194
|
+
const shouldPersistClientInfo =
|
|
195
|
+
requestClientInfo ||
|
|
196
|
+
typeof protocolVersion === "string" ||
|
|
197
|
+
typeof clientCapabilities !== "undefined";
|
|
198
|
+
|
|
199
|
+
const clientInfo = shouldPersistClientInfo
|
|
200
|
+
? {
|
|
201
|
+
name: normalizeString(requestClientInfo?.name) ?? "unknown",
|
|
202
|
+
title: normalizeString(requestClientInfo?.title),
|
|
203
|
+
version: normalizeString(requestClientInfo?.version),
|
|
204
|
+
platform: normalizeString(requestClientInfo?.platform),
|
|
205
|
+
vendor: normalizeString(requestClientInfo?.vendor),
|
|
206
|
+
persistentId: normalizeString(requestClientInfo?.persistentId),
|
|
207
|
+
clientId:
|
|
208
|
+
normalizeString(requestClientInfo?.clientId) ?? crypto.randomUUID(),
|
|
209
|
+
protocolVersion,
|
|
210
|
+
capabilities: clientCapabilities,
|
|
211
|
+
}
|
|
212
|
+
: undefined;
|
|
213
|
+
|
|
214
|
+
// Create session
|
|
215
|
+
const session = {
|
|
216
|
+
id: sessionId,
|
|
217
|
+
clientDid: request.clientDid || userDid, // Use provided clientDid or generated userDid
|
|
218
|
+
userDid: userDid, // Store generated user DID separately
|
|
219
|
+
agentDid: request.agentDid, // ✅ FIXED: Only agent DID, no fallback
|
|
220
|
+
serverDid: identity.did, // ✅ NEW: Server's DID (for clarity)
|
|
221
|
+
audience: request.audience,
|
|
222
|
+
createdAt: timestamp,
|
|
223
|
+
expiresAt: this.clock.calculateExpiry(
|
|
224
|
+
(this.config.session?.ttlMinutes || 30) * 60
|
|
225
|
+
),
|
|
226
|
+
clientInfo, // NEW: Store client information
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
this.sessions.set(sessionId, session);
|
|
230
|
+
|
|
231
|
+
// Create handshake response
|
|
232
|
+
const response = {
|
|
233
|
+
sessionId,
|
|
234
|
+
agentDid: identity.did,
|
|
235
|
+
timestamp,
|
|
236
|
+
capabilities: ["identity", "proof", "audit"],
|
|
237
|
+
...(userDid && { userDid }), // Include user DID in response if generated
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Sign the response
|
|
241
|
+
const signature = await this.signData(response);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
...response,
|
|
245
|
+
signature,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Process tool call with automatic proof generation
|
|
251
|
+
* Returns clean result only - proof is stored for out-of-band retrieval
|
|
252
|
+
*
|
|
253
|
+
* @param toolName - Name of the tool being called
|
|
254
|
+
* @param args - Tool arguments
|
|
255
|
+
* @param handler - Tool execution handler
|
|
256
|
+
* @param session - Session context (expected fields: id, audience, nonce?, delegationToken?, consentProof?)
|
|
257
|
+
*/
|
|
258
|
+
async processToolCall(
|
|
259
|
+
toolName: string,
|
|
260
|
+
args: any,
|
|
261
|
+
handler: (args: any) => Promise<any>,
|
|
262
|
+
session?: any
|
|
263
|
+
): Promise<any> {
|
|
264
|
+
// Check tool protection (delegation requirement)
|
|
265
|
+
if (this.config.toolProtectionService) {
|
|
266
|
+
// Get agent identity to check protection
|
|
267
|
+
const identity = await this.getIdentity();
|
|
268
|
+
|
|
269
|
+
if (this.config.audit?.enabled) {
|
|
270
|
+
console.log("[MCP-I] Checking tool protection:", {
|
|
271
|
+
tool: toolName,
|
|
272
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
273
|
+
hasDelegation: !!(session?.delegationToken || session?.consentProof),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const protection =
|
|
278
|
+
await this.config.toolProtectionService.checkToolProtection(
|
|
279
|
+
toolName,
|
|
280
|
+
identity.did
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (protection) {
|
|
284
|
+
// Tool requires delegation
|
|
285
|
+
const hasDelegation = session?.delegationToken || session?.consentProof;
|
|
286
|
+
|
|
287
|
+
if (!hasDelegation) {
|
|
288
|
+
// No delegation provided - intercept the tool call and store context
|
|
289
|
+
// Store intercepted tool call context for resumption
|
|
290
|
+
const interceptedCall: InterceptedToolCall = {
|
|
291
|
+
toolName,
|
|
292
|
+
args,
|
|
293
|
+
sessionId: session?.id || "unknown",
|
|
294
|
+
timestamp: this.clock.now(),
|
|
295
|
+
expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Generate resume token
|
|
299
|
+
const resumeToken = this.generateResumeToken(interceptedCall);
|
|
300
|
+
|
|
301
|
+
// Build consent URL with resume token
|
|
302
|
+
// Note: projectId is not available in base class - subclasses should override buildConsentUrl
|
|
303
|
+
const consentUrl = this.buildConsentUrl(
|
|
304
|
+
toolName,
|
|
305
|
+
protection.requiredScopes,
|
|
306
|
+
session,
|
|
307
|
+
resumeToken
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Create error with intercepted call context and pre-generated resume token
|
|
311
|
+
const error = new DelegationRequiredError(
|
|
312
|
+
toolName,
|
|
313
|
+
protection.requiredScopes,
|
|
314
|
+
consentUrl,
|
|
315
|
+
interceptedCall,
|
|
316
|
+
resumeToken
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Store intercepted call for resumption
|
|
320
|
+
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
321
|
+
|
|
322
|
+
// Clean up expired intercepted calls periodically
|
|
323
|
+
this.cleanupExpiredInterceptedCalls();
|
|
324
|
+
|
|
325
|
+
if (this.config.audit?.enabled) {
|
|
326
|
+
console.warn(
|
|
327
|
+
"[MCP-I] BLOCKED: Tool requires delegation but none provided",
|
|
328
|
+
{
|
|
329
|
+
tool: toolName,
|
|
330
|
+
requiredScopes: protection.requiredScopes,
|
|
331
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
332
|
+
resumeToken: error.resumeToken,
|
|
333
|
+
consentUrl,
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Delegation provided - verify it with AccessControlApiService
|
|
342
|
+
const delegationToken = session?.delegationToken;
|
|
343
|
+
const consentProof = session?.consentProof;
|
|
344
|
+
|
|
345
|
+
if (!this.accessControlService) {
|
|
346
|
+
// Access control service not available - log warning but allow execution
|
|
347
|
+
// This enables graceful degradation when service is not configured
|
|
348
|
+
if (this.config.audit?.enabled) {
|
|
349
|
+
console.warn(
|
|
350
|
+
"[MCP-I] ⚠️ Delegation token provided but AccessControlApiService not configured - skipping verification",
|
|
351
|
+
{
|
|
352
|
+
tool: toolName,
|
|
353
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
354
|
+
hasDelegationToken: !!delegationToken,
|
|
355
|
+
hasConsentProof: !!consentProof,
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
// Verify delegation token with AccessControlApiService
|
|
361
|
+
try {
|
|
362
|
+
if (this.config.audit?.enabled) {
|
|
363
|
+
console.log(
|
|
364
|
+
"[MCP-I] 🔐 Verifying delegation token with AccessControlApiService",
|
|
365
|
+
{
|
|
366
|
+
tool: toolName,
|
|
367
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
368
|
+
hasDelegationToken: !!delegationToken,
|
|
369
|
+
hasConsentProof: !!consentProof,
|
|
370
|
+
requiredScopes: protection.requiredScopes,
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Build verification request
|
|
376
|
+
const verifyRequest: VerifyDelegationRequest = {
|
|
377
|
+
agent_did: identity.did,
|
|
378
|
+
scopes: protection.requiredScopes,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Add delegation token if available (preferred over consent proof)
|
|
382
|
+
if (delegationToken) {
|
|
383
|
+
verifyRequest.delegation_token = delegationToken;
|
|
384
|
+
} else if (consentProof) {
|
|
385
|
+
// Consent proof is a JWT credential - use as credential_jwt
|
|
386
|
+
verifyRequest.credential_jwt = consentProof;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Add optional timestamp for verification
|
|
390
|
+
verifyRequest.timestamp = this.clock.now();
|
|
391
|
+
|
|
392
|
+
// Add client info from session if available
|
|
393
|
+
if (session?.clientDid || session?.clientId) {
|
|
394
|
+
verifyRequest.client_info = {
|
|
395
|
+
origin: session?.serverOrigin,
|
|
396
|
+
user_agent: session?.userAgent,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Perform verification
|
|
401
|
+
const verificationResult =
|
|
402
|
+
await this.accessControlService.verifyDelegation(verifyRequest, {
|
|
403
|
+
delegationToken: delegationToken || undefined,
|
|
404
|
+
credentialJwt: consentProof || undefined,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Check verification result
|
|
408
|
+
if (!verificationResult.data.valid) {
|
|
409
|
+
// Delegation verification failed
|
|
410
|
+
const reason =
|
|
411
|
+
verificationResult.data.reason ||
|
|
412
|
+
"Delegation token invalid or expired";
|
|
413
|
+
const errorDetails = verificationResult.data.error;
|
|
414
|
+
|
|
415
|
+
if (this.config.audit?.enabled) {
|
|
416
|
+
console.error("[MCP-I] ❌ Delegation verification FAILED", {
|
|
417
|
+
tool: toolName,
|
|
418
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
419
|
+
reason,
|
|
420
|
+
errorCode: errorDetails?.code,
|
|
421
|
+
errorMessage: errorDetails?.message,
|
|
422
|
+
requiredScopes: protection.requiredScopes,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Throw DelegationRequiredError to trigger consent flow
|
|
427
|
+
const interceptedCall: InterceptedToolCall = {
|
|
428
|
+
toolName,
|
|
429
|
+
args,
|
|
430
|
+
sessionId: session?.id || "unknown",
|
|
431
|
+
timestamp: this.clock.now(),
|
|
432
|
+
expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const resumeToken = this.generateResumeToken(interceptedCall);
|
|
436
|
+
const consentUrl = this.buildConsentUrl(
|
|
437
|
+
toolName,
|
|
438
|
+
protection.requiredScopes,
|
|
439
|
+
session,
|
|
440
|
+
resumeToken
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
444
|
+
this.cleanupExpiredInterceptedCalls();
|
|
445
|
+
|
|
446
|
+
throw new DelegationRequiredError(
|
|
447
|
+
toolName,
|
|
448
|
+
protection.requiredScopes,
|
|
449
|
+
consentUrl,
|
|
450
|
+
interceptedCall,
|
|
451
|
+
resumeToken
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ✅ SECURITY: Validate user_identifier matches session userDid
|
|
456
|
+
// This ensures delegations are user-specific and prevents user isolation bypass
|
|
457
|
+
const credential = verificationResult.data.credential;
|
|
458
|
+
const delegationUserIdentifier = credential?.user_identifier;
|
|
459
|
+
const sessionUserDid = session?.userDid;
|
|
460
|
+
|
|
461
|
+
if (delegationUserIdentifier && sessionUserDid) {
|
|
462
|
+
if (delegationUserIdentifier !== sessionUserDid) {
|
|
463
|
+
// User identifier mismatch - potential security issue
|
|
464
|
+
const securityError = `Delegation user_identifier mismatch: delegation has "${delegationUserIdentifier.substring(0, 20)}..." but session has "${sessionUserDid.substring(0, 20)}..."`;
|
|
465
|
+
|
|
466
|
+
if (this.config.audit?.enabled) {
|
|
467
|
+
console.error(
|
|
468
|
+
"[MCP-I] 🔒 SECURITY: User identifier validation FAILED",
|
|
469
|
+
{
|
|
470
|
+
tool: toolName,
|
|
471
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
472
|
+
delegationUserIdentifier:
|
|
473
|
+
delegationUserIdentifier.substring(0, 20) + "...",
|
|
474
|
+
sessionUserDid: sessionUserDid.substring(0, 20) + "...",
|
|
475
|
+
sessionId: session?.id?.substring(0, 20) + "...",
|
|
476
|
+
reason: "user_identifier_mismatch",
|
|
477
|
+
severity: "high",
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Throw DelegationRequiredError to force re-authentication
|
|
483
|
+
const interceptedCall: InterceptedToolCall = {
|
|
484
|
+
toolName,
|
|
485
|
+
args,
|
|
486
|
+
sessionId: session?.id || "unknown",
|
|
487
|
+
timestamp: this.clock.now(),
|
|
488
|
+
expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const resumeToken = this.generateResumeToken(interceptedCall);
|
|
492
|
+
const consentUrl = this.buildConsentUrl(
|
|
493
|
+
toolName,
|
|
494
|
+
protection.requiredScopes,
|
|
495
|
+
session,
|
|
496
|
+
resumeToken
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
500
|
+
this.cleanupExpiredInterceptedCalls();
|
|
501
|
+
|
|
502
|
+
throw new DelegationRequiredError(
|
|
503
|
+
toolName,
|
|
504
|
+
protection.requiredScopes,
|
|
505
|
+
consentUrl,
|
|
506
|
+
interceptedCall,
|
|
507
|
+
resumeToken
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// User identifier matches - log success for audit
|
|
512
|
+
if (this.config.audit?.enabled) {
|
|
513
|
+
console.log("[MCP-I] ✅ User identifier validation PASSED", {
|
|
514
|
+
tool: toolName,
|
|
515
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
516
|
+
userDid: sessionUserDid.substring(0, 20) + "...",
|
|
517
|
+
sessionId: session?.id?.substring(0, 20) + "...",
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
} else if (delegationUserIdentifier && !sessionUserDid) {
|
|
521
|
+
// Delegation has user_identifier but session doesn't - log warning
|
|
522
|
+
if (this.config.audit?.enabled) {
|
|
523
|
+
console.warn(
|
|
524
|
+
"[MCP-I] ⚠️ Delegation has user_identifier but session missing userDid",
|
|
525
|
+
{
|
|
526
|
+
tool: toolName,
|
|
527
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
528
|
+
delegationUserIdentifier:
|
|
529
|
+
delegationUserIdentifier.substring(0, 20) + "...",
|
|
530
|
+
sessionId: session?.id?.substring(0, 20) + "...",
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Verification succeeded
|
|
537
|
+
if (this.config.audit?.enabled) {
|
|
538
|
+
console.log("[MCP-I] ✅ Delegation verification SUCCEEDED", {
|
|
539
|
+
tool: toolName,
|
|
540
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
541
|
+
delegationId: verificationResult.data.delegation_id,
|
|
542
|
+
credentialScopes: verificationResult.data.credential?.scopes,
|
|
543
|
+
requiredScopes: protection.requiredScopes,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} catch (error: any) {
|
|
547
|
+
// Handle verification errors
|
|
548
|
+
if (error instanceof DelegationRequiredError) {
|
|
549
|
+
// Re-throw DelegationRequiredError as-is (already handled above)
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Handle AgentShieldAPIError (network errors, API errors, etc.)
|
|
554
|
+
if (error instanceof AgentShieldAPIError) {
|
|
555
|
+
if (this.config.audit?.enabled) {
|
|
556
|
+
console.error(
|
|
557
|
+
"[MCP-I] ❌ Delegation verification error (API failure)",
|
|
558
|
+
{
|
|
559
|
+
tool: toolName,
|
|
560
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
561
|
+
errorCode: error.code,
|
|
562
|
+
errorMessage: error.message,
|
|
563
|
+
errorDetails: error.details,
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// On API errors, fail securely by requiring delegation
|
|
569
|
+
// This prevents unauthorized access when verification service is unavailable
|
|
570
|
+
const interceptedCall: InterceptedToolCall = {
|
|
571
|
+
toolName,
|
|
572
|
+
args,
|
|
573
|
+
sessionId: session?.id || "unknown",
|
|
574
|
+
timestamp: this.clock.now(),
|
|
575
|
+
expiresAt: this.clock.calculateExpiry(1800),
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const resumeToken = this.generateResumeToken(interceptedCall);
|
|
579
|
+
const consentUrl = this.buildConsentUrl(
|
|
580
|
+
toolName,
|
|
581
|
+
protection.requiredScopes,
|
|
582
|
+
session,
|
|
583
|
+
resumeToken
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
587
|
+
this.cleanupExpiredInterceptedCalls();
|
|
588
|
+
|
|
589
|
+
throw new DelegationRequiredError(
|
|
590
|
+
toolName,
|
|
591
|
+
protection.requiredScopes,
|
|
592
|
+
consentUrl,
|
|
593
|
+
interceptedCall,
|
|
594
|
+
resumeToken
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Unexpected error - log and fail securely
|
|
599
|
+
if (this.config.audit?.enabled) {
|
|
600
|
+
console.error(
|
|
601
|
+
"[MCP-I] ❌ Unexpected error during delegation verification",
|
|
602
|
+
{
|
|
603
|
+
tool: toolName,
|
|
604
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
605
|
+
error: error.message || String(error),
|
|
606
|
+
errorStack: error.stack,
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Fail securely - require delegation on unexpected errors
|
|
612
|
+
const interceptedCall: InterceptedToolCall = {
|
|
613
|
+
toolName,
|
|
614
|
+
args,
|
|
615
|
+
sessionId: session?.id || "unknown",
|
|
616
|
+
timestamp: this.clock.now(),
|
|
617
|
+
expiresAt: this.clock.calculateExpiry(1800),
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const resumeToken = this.generateResumeToken(interceptedCall);
|
|
621
|
+
const consentUrl = this.buildConsentUrl(
|
|
622
|
+
toolName,
|
|
623
|
+
protection.requiredScopes,
|
|
624
|
+
session,
|
|
625
|
+
resumeToken
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
this.interceptedCalls.set(resumeToken, interceptedCall);
|
|
629
|
+
this.cleanupExpiredInterceptedCalls();
|
|
630
|
+
|
|
631
|
+
throw new DelegationRequiredError(
|
|
632
|
+
toolName,
|
|
633
|
+
protection.requiredScopes,
|
|
634
|
+
consentUrl,
|
|
635
|
+
interceptedCall,
|
|
636
|
+
resumeToken
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
// No protection required - tool can be executed freely
|
|
642
|
+
if (this.config.audit?.enabled) {
|
|
643
|
+
console.log(
|
|
644
|
+
"[MCP-I] Tool protection check passed (no delegation required)",
|
|
645
|
+
{
|
|
646
|
+
tool: toolName,
|
|
647
|
+
agentDid: identity.did.slice(0, 20) + "...",
|
|
648
|
+
reason: "Tool not configured to require delegation",
|
|
649
|
+
}
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Execute the tool
|
|
656
|
+
const result = await handler(args);
|
|
657
|
+
|
|
658
|
+
// Create proof
|
|
659
|
+
const proof = await this.createProof(result, session);
|
|
660
|
+
|
|
661
|
+
// Store proof for out-of-band retrieval
|
|
662
|
+
this.lastProof = proof;
|
|
663
|
+
|
|
664
|
+
// Log if audit is enabled
|
|
665
|
+
if (this.config.audit?.enabled) {
|
|
666
|
+
this.logAudit("tool_executed", {
|
|
667
|
+
tool: toolName,
|
|
668
|
+
sessionId: session?.id,
|
|
669
|
+
timestamp: this.clock.now(),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Return clean result only (no proof in LLM context)
|
|
674
|
+
return result;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Resume a tool call after authorization
|
|
679
|
+
*
|
|
680
|
+
* @param resumeToken - Token from DelegationRequiredError
|
|
681
|
+
* @param handler - Tool execution handler
|
|
682
|
+
* @param delegationToken - Delegation token from authorization
|
|
683
|
+
* @returns Tool execution result
|
|
684
|
+
*/
|
|
685
|
+
async resumeToolCall(
|
|
686
|
+
resumeToken: string,
|
|
687
|
+
handler: (args: any) => Promise<any>,
|
|
688
|
+
delegationToken?: string
|
|
689
|
+
): Promise<any> {
|
|
690
|
+
const interceptedCall = this.interceptedCalls.get(resumeToken);
|
|
691
|
+
|
|
692
|
+
if (!interceptedCall) {
|
|
693
|
+
throw new Error(`Invalid or expired resume token: ${resumeToken}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Check if call has expired
|
|
697
|
+
if (this.clock.hasExpired(interceptedCall.expiresAt)) {
|
|
698
|
+
this.interceptedCalls.delete(resumeToken);
|
|
699
|
+
throw new Error(`Resume token expired: ${resumeToken}`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Get session for the intercepted call
|
|
703
|
+
const session = this.sessions.get(interceptedCall.sessionId);
|
|
704
|
+
if (!session) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`Session not found for intercepted call: ${interceptedCall.sessionId}`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Add delegation token to session
|
|
711
|
+
const enhancedSession = {
|
|
712
|
+
...session,
|
|
713
|
+
delegationToken,
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// Resume the tool call with delegation
|
|
717
|
+
const result = await this.processToolCall(
|
|
718
|
+
interceptedCall.toolName,
|
|
719
|
+
interceptedCall.args,
|
|
720
|
+
handler,
|
|
721
|
+
enhancedSession
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
// Clean up intercepted call after successful resumption to prevent memory leak
|
|
725
|
+
this.interceptedCalls.delete(resumeToken);
|
|
726
|
+
|
|
727
|
+
return result;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Generate a resume token for intercepted tool call
|
|
732
|
+
*/
|
|
733
|
+
private generateResumeToken(call: InterceptedToolCall): string {
|
|
734
|
+
// Create a deterministic token from the call context
|
|
735
|
+
const tokenData = JSON.stringify({
|
|
736
|
+
tool: call.toolName,
|
|
737
|
+
args: call.args,
|
|
738
|
+
sessionId: call.sessionId,
|
|
739
|
+
timestamp: call.timestamp,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// Simple hash-based token (in production, use proper crypto)
|
|
743
|
+
let hash = 0;
|
|
744
|
+
for (let i = 0; i < tokenData.length; i++) {
|
|
745
|
+
const char = tokenData.charCodeAt(i);
|
|
746
|
+
hash = (hash << 5) - hash + char;
|
|
747
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return `resume_${Math.abs(hash).toString(36)}_${Date.now().toString(36)}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Clean up expired intercepted calls
|
|
755
|
+
*/
|
|
756
|
+
private cleanupExpiredInterceptedCalls(): void {
|
|
757
|
+
const now = this.clock.now();
|
|
758
|
+
for (const [token, call] of this.interceptedCalls.entries()) {
|
|
759
|
+
if (this.clock.hasExpired(call.expiresAt)) {
|
|
760
|
+
this.interceptedCalls.delete(token);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Build consent URL for AgentShield delegation flow
|
|
767
|
+
*
|
|
768
|
+
* Note: Parameter names use snake_case for AgentShield API compatibility.
|
|
769
|
+
* This is documented in docs/API_PARITY_GUIDE.md under "Field Naming Conventions".
|
|
770
|
+
*
|
|
771
|
+
* AgentShield API requires snake_case in URL parameters:
|
|
772
|
+
* - session_id (not sessionId)
|
|
773
|
+
* - agent_did (not agentDid)
|
|
774
|
+
* - resume_token (not resumeToken)
|
|
775
|
+
*
|
|
776
|
+
* @param toolName - Tool that requires delegation
|
|
777
|
+
* @param scopes - Required scopes for the tool
|
|
778
|
+
* @param session - Current session context
|
|
779
|
+
* @param resumeToken - Token to resume after delegation
|
|
780
|
+
* @param projectId - Project ID for AgentShield API
|
|
781
|
+
* @returns Full consent URL with snake_case parameters
|
|
782
|
+
*/
|
|
783
|
+
protected buildConsentUrl(
|
|
784
|
+
toolName: string,
|
|
785
|
+
scopes: string[],
|
|
786
|
+
session?: any,
|
|
787
|
+
resumeToken?: string,
|
|
788
|
+
projectId?: string
|
|
789
|
+
): string {
|
|
790
|
+
// Default implementation - override in subclasses
|
|
791
|
+
// This URL should point to AgentShield's consent page
|
|
792
|
+
// Parameter names use snake_case for AgentShield API compatibility
|
|
793
|
+
const params = new URLSearchParams({
|
|
794
|
+
tool: toolName,
|
|
795
|
+
scopes: scopes.join(","),
|
|
796
|
+
session_id: session?.id || "",
|
|
797
|
+
agent_did: session?.agentDid || "",
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Add project_id if provided (required for AgentShield consent endpoint)
|
|
801
|
+
if (projectId) {
|
|
802
|
+
params.set("project_id", projectId);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Add resume token if provided
|
|
806
|
+
if (resumeToken) {
|
|
807
|
+
params.set("resume_token", resumeToken);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Use AgentShield consent endpoint
|
|
811
|
+
return `https://kya.vouched.id/bouncer/consent?${params.toString()}`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Issue a new nonce and register it in the cache
|
|
816
|
+
* Use this to get a nonce for the session context before calling processToolCall
|
|
817
|
+
*/
|
|
818
|
+
async issueNonce(sessionId: string): Promise<string> {
|
|
819
|
+
const nonce = await this.generateNonce();
|
|
820
|
+
// Get session to extract agentDid for agent-scoped nonce caching
|
|
821
|
+
const session = this.sessions.get(sessionId);
|
|
822
|
+
const agentDid = session?.agentDid || (await this.getIdentity()).did;
|
|
823
|
+
await this.nonceCache.add(
|
|
824
|
+
nonce,
|
|
825
|
+
300, // 5 minute expiry in seconds
|
|
826
|
+
agentDid // Agent-scoped nonce to prevent cross-agent replay attacks
|
|
827
|
+
);
|
|
828
|
+
return nonce;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Create cryptographic proof for data
|
|
833
|
+
*/
|
|
834
|
+
async createProof(data: any, session?: any): Promise<any> {
|
|
835
|
+
const identity = await this.getIdentity();
|
|
836
|
+
const timestamp = this.clock.now();
|
|
837
|
+
|
|
838
|
+
// Use nonce from session if provided, otherwise generate new one
|
|
839
|
+
let nonce: string;
|
|
840
|
+
if (session?.nonce) {
|
|
841
|
+
nonce = session.nonce;
|
|
842
|
+
} else {
|
|
843
|
+
nonce = await this.generateNonce();
|
|
844
|
+
// Add nonce to cache to prevent replay (agent-scoped to prevent cross-agent replay attacks)
|
|
845
|
+
const agentDid = session?.agentDid || identity.did;
|
|
846
|
+
await this.nonceCache.add(
|
|
847
|
+
nonce,
|
|
848
|
+
300, // 5 minute expiry in seconds
|
|
849
|
+
agentDid // Agent-scoped nonce to prevent cross-agent replay attacks
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const proofData = {
|
|
854
|
+
data,
|
|
855
|
+
timestamp,
|
|
856
|
+
nonce,
|
|
857
|
+
did: identity.did,
|
|
858
|
+
sessionId: session?.id,
|
|
859
|
+
audience: session?.audience,
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const signature = await this.signData(proofData);
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
timestamp,
|
|
866
|
+
nonce,
|
|
867
|
+
did: identity.did,
|
|
868
|
+
signature,
|
|
869
|
+
algorithm: "Ed25519",
|
|
870
|
+
sessionId: session?.id,
|
|
871
|
+
audience: session?.audience,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Verify a proof
|
|
877
|
+
*
|
|
878
|
+
* Supports both old format (data, proof) and new DetachedProof format.
|
|
879
|
+
* When DetachedProof format is used, ProofVerifier is used if available.
|
|
880
|
+
*
|
|
881
|
+
* @param dataOrProof - Either raw data (old format) or DetachedProof (new format)
|
|
882
|
+
* @param proofOrSession - Either proof object (old format) or session context (new format)
|
|
883
|
+
* @returns true if proof is valid, false otherwise
|
|
884
|
+
*/
|
|
885
|
+
async verifyProof(dataOrProof: any, proofOrSession?: any): Promise<boolean> {
|
|
886
|
+
// Check if first argument is DetachedProof format
|
|
887
|
+
if (
|
|
888
|
+
dataOrProof &&
|
|
889
|
+
typeof dataOrProof === "object" &&
|
|
890
|
+
"jws" in dataOrProof &&
|
|
891
|
+
"meta" in dataOrProof
|
|
892
|
+
) {
|
|
893
|
+
// New DetachedProof format
|
|
894
|
+
const detachedProof = dataOrProof as DetachedProof;
|
|
895
|
+
const session = proofOrSession;
|
|
896
|
+
|
|
897
|
+
// Use ProofVerifier if available
|
|
898
|
+
if (this.proofVerifier) {
|
|
899
|
+
try {
|
|
900
|
+
// Resolve DID to get public key
|
|
901
|
+
const didDoc = await this.fetch.resolveDID(detachedProof.meta.did);
|
|
902
|
+
const publicKeyJwk = this.extractPublicKeyJwk(
|
|
903
|
+
didDoc,
|
|
904
|
+
detachedProof.meta.kid
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
if (!publicKeyJwk) {
|
|
908
|
+
console.error("[MCPIRuntimeBase] Failed to extract public key JWK");
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Verify proof using ProofVerifier
|
|
913
|
+
const result = await this.proofVerifier.verifyProof(
|
|
914
|
+
detachedProof,
|
|
915
|
+
publicKeyJwk
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
if (result.valid && session) {
|
|
919
|
+
// Store canonical payload in session for detached JWS consumers
|
|
920
|
+
const canonicalPayload = this.proofVerifier.buildCanonicalPayload(
|
|
921
|
+
detachedProof.meta
|
|
922
|
+
);
|
|
923
|
+
session.canonicalPayload = canonicalPayload;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return result.valid;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
console.error("[MCPIRuntimeBase] Proof verification failed:", error);
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
// Fallback to old verification if ProofVerifier not available
|
|
933
|
+
console.warn(
|
|
934
|
+
"[MCPIRuntimeBase] ProofVerifier not available, using fallback verification"
|
|
935
|
+
);
|
|
936
|
+
return this.verifyProofLegacy(dataOrProof, proofOrSession);
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
// Old format (data, proof)
|
|
940
|
+
return this.verifyProofLegacy(dataOrProof, proofOrSession);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Legacy proof verification (backward compatibility)
|
|
946
|
+
* @internal
|
|
947
|
+
*/
|
|
948
|
+
private async verifyProofLegacy(data: any, proof: any): Promise<boolean> {
|
|
949
|
+
try {
|
|
950
|
+
// Check nonce hasn't been used (scoped to agent DID to prevent cross-agent replay attacks)
|
|
951
|
+
if (await this.nonceCache.has(proof.nonce, proof.did)) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Check timestamp is within skew
|
|
956
|
+
if (
|
|
957
|
+
!this.clock.isWithinSkew(
|
|
958
|
+
proof.timestamp,
|
|
959
|
+
this.config.session?.timestampSkewSeconds || 120
|
|
960
|
+
)
|
|
961
|
+
) {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Resolve DID to get public key
|
|
966
|
+
const didDoc = await this.fetch.resolveDID(proof.did);
|
|
967
|
+
const publicKey = this.extractPublicKey(didDoc);
|
|
968
|
+
|
|
969
|
+
// Verify signature
|
|
970
|
+
const proofData = {
|
|
971
|
+
data,
|
|
972
|
+
timestamp: proof.timestamp,
|
|
973
|
+
nonce: proof.nonce,
|
|
974
|
+
did: proof.did,
|
|
975
|
+
sessionId: proof.sessionId,
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(proofData));
|
|
979
|
+
const signatureBytes = this.base64ToBytes(proof.signature);
|
|
980
|
+
|
|
981
|
+
const isValid = await this.crypto.verify(
|
|
982
|
+
dataBytes,
|
|
983
|
+
signatureBytes,
|
|
984
|
+
publicKey
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
// If signature is valid, add nonce to cache to prevent replay (scoped to agent DID)
|
|
988
|
+
if (isValid) {
|
|
989
|
+
// Pass TTL in seconds, not absolute timestamp
|
|
990
|
+
const ttlSeconds = (this.config.session?.ttlMinutes || 30) * 60; // Convert minutes to seconds
|
|
991
|
+
await this.nonceCache.add(proof.nonce, ttlSeconds, proof.did);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return isValid;
|
|
995
|
+
} catch (error) {
|
|
996
|
+
console.error("Proof verification failed:", error);
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Verify a JWS proof (full compact JWS format: header.payload.signature)
|
|
1003
|
+
*
|
|
1004
|
+
* This method provides full cryptographic signature verification using CryptoService.
|
|
1005
|
+
* Use this when you have a DetachedProof with JWS instead of raw signature.
|
|
1006
|
+
*
|
|
1007
|
+
* @param jws - Full compact JWS string (header.payload.signature)
|
|
1008
|
+
* @param publicKeyJwk - Ed25519 public key in JWK format
|
|
1009
|
+
* @param detachedPayload - Optional detached payload for detached JWS format
|
|
1010
|
+
* @returns true if signature is valid, false otherwise
|
|
1011
|
+
*/
|
|
1012
|
+
async verifyProofJWS(
|
|
1013
|
+
jws: string,
|
|
1014
|
+
publicKeyJwk: Ed25519JWK,
|
|
1015
|
+
detachedPayload?: string | Uint8Array
|
|
1016
|
+
): Promise<boolean> {
|
|
1017
|
+
if (!this.cryptoService) {
|
|
1018
|
+
console.error("[MCPIRuntimeBase] CryptoService not initialized");
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
const options =
|
|
1024
|
+
detachedPayload !== undefined ? { detachedPayload } : undefined;
|
|
1025
|
+
return await this.cryptoService.verifyJWS(jws, publicKeyJwk, options);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
console.error("[MCPIRuntimeBase] JWS verification failed:", error);
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Get current session
|
|
1034
|
+
*/
|
|
1035
|
+
async getCurrentSession(): Promise<any> {
|
|
1036
|
+
// Find non-expired session
|
|
1037
|
+
for (const [id, session] of this.sessions) {
|
|
1038
|
+
if (!this.clock.hasExpired(session.expiresAt)) {
|
|
1039
|
+
return session;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Get the last generated proof for out-of-band transport
|
|
1047
|
+
*/
|
|
1048
|
+
getLastProof(): any {
|
|
1049
|
+
return this.lastProof;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Create well-known handler for identity verification
|
|
1054
|
+
*/
|
|
1055
|
+
createWellKnownHandler(
|
|
1056
|
+
config?: WellKnownConfig
|
|
1057
|
+
): (path: string) => Promise<WellKnownResponse | MCPIdentity | null> {
|
|
1058
|
+
return async (path: string) => {
|
|
1059
|
+
const identity = await this.getIdentity();
|
|
1060
|
+
|
|
1061
|
+
if (path === "/.well-known/did.json") {
|
|
1062
|
+
const didDocument = this.createDIDDocument(identity);
|
|
1063
|
+
return {
|
|
1064
|
+
status: 200,
|
|
1065
|
+
headers: {
|
|
1066
|
+
"Content-Type": "application/did+json",
|
|
1067
|
+
"Cache-Control":
|
|
1068
|
+
this.config.environment === "production"
|
|
1069
|
+
? "public, max-age=300"
|
|
1070
|
+
: "no-store",
|
|
1071
|
+
},
|
|
1072
|
+
body: JSON.stringify(didDocument, null, 2),
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (path === "/.well-known/agent.json") {
|
|
1077
|
+
const agentDocument: AgentDocument = {
|
|
1078
|
+
id: identity.did,
|
|
1079
|
+
capabilities: {
|
|
1080
|
+
"mcp-i": ["handshake", "signing", "verification"],
|
|
1081
|
+
},
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
if (config?.serviceName || config?.serviceEndpoint) {
|
|
1085
|
+
agentDocument.metadata = {
|
|
1086
|
+
...(config?.serviceName && { name: config.serviceName }),
|
|
1087
|
+
...(config?.serviceEndpoint && {
|
|
1088
|
+
serviceEndpoint: config.serviceEndpoint,
|
|
1089
|
+
}),
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return {
|
|
1094
|
+
status: 200,
|
|
1095
|
+
headers: {
|
|
1096
|
+
"Content-Type": "application/json",
|
|
1097
|
+
"Cache-Control":
|
|
1098
|
+
this.config.environment === "production"
|
|
1099
|
+
? "public, max-age=300"
|
|
1100
|
+
: "no-store",
|
|
1101
|
+
},
|
|
1102
|
+
body: JSON.stringify(agentDocument, null, 2),
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (path === "/.well-known/mcp-identity") {
|
|
1107
|
+
return {
|
|
1108
|
+
did: identity.did,
|
|
1109
|
+
publicKey: identity.publicKey,
|
|
1110
|
+
serviceName: config?.serviceName || "MCP-I Service",
|
|
1111
|
+
serviceEndpoint: config?.serviceEndpoint || "https://example.com",
|
|
1112
|
+
timestamp: this.clock.now(),
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return null;
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Create debug endpoint (development only)
|
|
1122
|
+
*/
|
|
1123
|
+
createDebugEndpoint(): any {
|
|
1124
|
+
if (this.config.environment === "production") {
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return async () => {
|
|
1129
|
+
const identity = await this.getIdentity();
|
|
1130
|
+
const session = await this.getCurrentSession();
|
|
1131
|
+
|
|
1132
|
+
return {
|
|
1133
|
+
identity: {
|
|
1134
|
+
did: identity.did,
|
|
1135
|
+
publicKey: identity.publicKey,
|
|
1136
|
+
},
|
|
1137
|
+
session,
|
|
1138
|
+
config: {
|
|
1139
|
+
environment: this.config.environment,
|
|
1140
|
+
timestampSkewSeconds: this.config.session?.timestampSkewSeconds,
|
|
1141
|
+
sessionTtlMinutes: this.config.session?.ttlMinutes,
|
|
1142
|
+
},
|
|
1143
|
+
timestamp: this.clock.now(),
|
|
1144
|
+
};
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Get audit logger
|
|
1150
|
+
*/
|
|
1151
|
+
getAuditLogger(): any {
|
|
1152
|
+
return {
|
|
1153
|
+
log: (event: string, data: any) => this.logAudit(event, data),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Rotate keys
|
|
1159
|
+
*/
|
|
1160
|
+
async rotateKeys(): Promise<AgentIdentity> {
|
|
1161
|
+
const oldDid = this.cachedIdentity?.did;
|
|
1162
|
+
const newIdentity = await this.identity.rotateKeys();
|
|
1163
|
+
this.cachedIdentity = newIdentity;
|
|
1164
|
+
|
|
1165
|
+
if (this.config.audit?.enabled) {
|
|
1166
|
+
this.logAudit("keys_rotated", {
|
|
1167
|
+
oldDid: oldDid,
|
|
1168
|
+
newDid: newIdentity.did,
|
|
1169
|
+
timestamp: this.clock.now(),
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return newIdentity;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Helper methods
|
|
1177
|
+
|
|
1178
|
+
private async signData(data: any): Promise<string> {
|
|
1179
|
+
const identity = await this.getIdentity();
|
|
1180
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(data));
|
|
1181
|
+
const signature = await this.crypto.sign(dataBytes, identity.privateKey);
|
|
1182
|
+
return this.bytesToBase64(signature);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private async generateNonce(): Promise<string> {
|
|
1186
|
+
const bytes = await this.crypto.randomBytes(32);
|
|
1187
|
+
return this.bytesToBase64(bytes);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
private async generateSessionId(): Promise<string> {
|
|
1191
|
+
const bytes = await this.crypto.randomBytes(16);
|
|
1192
|
+
return this.bytesToHex(bytes);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Log structured events in JSON format (NOT frozen audit format).
|
|
1197
|
+
*
|
|
1198
|
+
* **Important:** This method logs events in JSON format for general runtime events
|
|
1199
|
+
* (e.g., "runtime_initialized", "tool_executed", "keys_rotated").
|
|
1200
|
+
*
|
|
1201
|
+
* **For frozen format audit logs** (MCP-I spec compliance), use `AuditLogger.logAuditRecord()`
|
|
1202
|
+
* from `@kya-os/mcp-i/runtime` instead. The frozen format is:
|
|
1203
|
+
* ```
|
|
1204
|
+
* audit.v1 ts=<unix> session=<id> audience=<host> did=<did> kid=<kid> reqHash=<sha256:..> resHash=<sha256:..> verified=yes|no scope=<scopeId|->
|
|
1205
|
+
* ```
|
|
1206
|
+
*
|
|
1207
|
+
* **Format:** JSON object with `event`, `data`, `timestamp`, and `timestampFormatted` fields.
|
|
1208
|
+
*
|
|
1209
|
+
* **Privacy:** If `includePayloads` is false (default), the `data` field is omitted.
|
|
1210
|
+
*
|
|
1211
|
+
* **Use Cases:**
|
|
1212
|
+
* - Developer debugging and local logging
|
|
1213
|
+
* - Runtime initialization events
|
|
1214
|
+
* - Tool execution tracking (non-spec-compliant)
|
|
1215
|
+
* - Key rotation events
|
|
1216
|
+
*
|
|
1217
|
+
* **NOT for:**
|
|
1218
|
+
* - MCP-I spec-compliant audit logs (use `AuditLogger`)
|
|
1219
|
+
* - Production audit trails (use `AuditLogger`)
|
|
1220
|
+
* - Compliance requirements (use `AuditLogger`)
|
|
1221
|
+
*
|
|
1222
|
+
* @param event - Event name (e.g., "runtime_initialized", "tool_executed")
|
|
1223
|
+
* @param data - Event data (only included if `includePayloads` is true)
|
|
1224
|
+
*
|
|
1225
|
+
* @example
|
|
1226
|
+
* ```typescript
|
|
1227
|
+
* // Logs: {"event":"runtime_initialized","timestamp":1234567890,"timestampFormatted":"2024-01-01T00:00:00Z"}
|
|
1228
|
+
* this.logAudit("runtime_initialized", { did: "did:key:..." });
|
|
1229
|
+
* ```
|
|
1230
|
+
*
|
|
1231
|
+
* @internal This is a private method for internal runtime events.
|
|
1232
|
+
* Use AuditLogger for spec-compliant frozen format audit logs.
|
|
1233
|
+
*/
|
|
1234
|
+
private logAudit(event: string, data: any): void {
|
|
1235
|
+
if (!this.config.audit?.enabled) return;
|
|
1236
|
+
|
|
1237
|
+
const record = {
|
|
1238
|
+
event,
|
|
1239
|
+
data: this.config.audit.includePayloads ? data : undefined,
|
|
1240
|
+
timestamp: this.clock.now(),
|
|
1241
|
+
timestampFormatted: this.clock.format(this.clock.now()),
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
const logLine = JSON.stringify(record);
|
|
1245
|
+
|
|
1246
|
+
if (this.config.audit.logFunction) {
|
|
1247
|
+
this.config.audit.logFunction(logLine);
|
|
1248
|
+
} else {
|
|
1249
|
+
console.log("[AUDIT]", logLine);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private createDIDDocument(identity: AgentIdentity): DIDDocument {
|
|
1254
|
+
return {
|
|
1255
|
+
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
1256
|
+
id: identity.did,
|
|
1257
|
+
verificationMethod: [
|
|
1258
|
+
{
|
|
1259
|
+
id: `${identity.did}#key-1`,
|
|
1260
|
+
type: "Ed25519VerificationKey2020",
|
|
1261
|
+
controller: identity.did,
|
|
1262
|
+
// Using base64 for cross-platform compatibility (Cloudflare Workers)
|
|
1263
|
+
// W3C DID spec supports both base64 and multibase formats
|
|
1264
|
+
publicKeyBase64: identity.publicKey,
|
|
1265
|
+
},
|
|
1266
|
+
],
|
|
1267
|
+
authentication: [`${identity.did}#key-1`],
|
|
1268
|
+
assertionMethod: [`${identity.did}#key-1`],
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
private extractPublicKey(didDoc: any): string {
|
|
1273
|
+
const method = didDoc.verificationMethod?.[0];
|
|
1274
|
+
if (method?.publicKeyBase64) {
|
|
1275
|
+
return method.publicKeyBase64;
|
|
1276
|
+
}
|
|
1277
|
+
if (method?.publicKeyMultibase) {
|
|
1278
|
+
// Convert multibase to base64
|
|
1279
|
+
return method.publicKeyMultibase; // Simplified
|
|
1280
|
+
}
|
|
1281
|
+
throw new Error("Public key not found in DID document");
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Extract public key JWK from DID document
|
|
1286
|
+
*/
|
|
1287
|
+
private extractPublicKeyJwk(didDoc: any, kid?: string): Ed25519JWK | null {
|
|
1288
|
+
// Try to find Ed25519 public key matching kid if provided
|
|
1289
|
+
const verificationMethod =
|
|
1290
|
+
didDoc.verificationMethod?.find((vm: any) => {
|
|
1291
|
+
const matchesType =
|
|
1292
|
+
vm.type === "Ed25519VerificationKey2020" ||
|
|
1293
|
+
vm.type === "JsonWebKey2020";
|
|
1294
|
+
const matchesKid = !kid || vm.id === kid || vm.id.endsWith(`#${kid}`);
|
|
1295
|
+
return matchesType && matchesKid;
|
|
1296
|
+
}) || didDoc.verificationMethod?.[0]; // Fallback to first method
|
|
1297
|
+
|
|
1298
|
+
if (verificationMethod?.publicKeyJwk) {
|
|
1299
|
+
const jwk = verificationMethod.publicKeyJwk;
|
|
1300
|
+
// Ensure it's Ed25519 format
|
|
1301
|
+
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
|
|
1302
|
+
return jwk as Ed25519JWK;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Fallback: try to convert multibase to JWK (simplified)
|
|
1307
|
+
if (verificationMethod?.publicKeyMultibase) {
|
|
1308
|
+
// This is a simplified conversion - in production, use proper multibase decoding
|
|
1309
|
+
console.warn(
|
|
1310
|
+
"[MCPIRuntimeBase] Multibase to JWK conversion not fully implemented"
|
|
1311
|
+
);
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
private bytesToBase64(bytes: Uint8Array): string {
|
|
1319
|
+
return Buffer.from(bytes).toString("base64");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
private base64ToBytes(base64: string): Uint8Array {
|
|
1323
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
private bytesToHex(bytes: Uint8Array): string {
|
|
1327
|
+
return Buffer.from(bytes).toString("hex");
|
|
1328
|
+
}
|
|
1329
|
+
}
|