@mcp-i/core 0.1.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/LICENSE +21 -0
- package/README.md +390 -0
- package/dist/auth/handshake.d.ts +104 -0
- package/dist/auth/handshake.d.ts.map +1 -0
- package/dist/auth/handshake.js +230 -0
- package/dist/auth/handshake.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/types.d.ts +31 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +7 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/delegation/audience-validator.d.ts +9 -0
- package/dist/delegation/audience-validator.d.ts.map +1 -0
- package/dist/delegation/audience-validator.js +17 -0
- package/dist/delegation/audience-validator.js.map +1 -0
- package/dist/delegation/bitstring.d.ts +37 -0
- package/dist/delegation/bitstring.d.ts.map +1 -0
- package/dist/delegation/bitstring.js +117 -0
- package/dist/delegation/bitstring.js.map +1 -0
- package/dist/delegation/cascading-revocation.d.ts +45 -0
- package/dist/delegation/cascading-revocation.d.ts.map +1 -0
- package/dist/delegation/cascading-revocation.js +148 -0
- package/dist/delegation/cascading-revocation.js.map +1 -0
- package/dist/delegation/delegation-graph.d.ts +49 -0
- package/dist/delegation/delegation-graph.d.ts.map +1 -0
- package/dist/delegation/delegation-graph.js +99 -0
- package/dist/delegation/delegation-graph.js.map +1 -0
- package/dist/delegation/did-key-resolver.d.ts +64 -0
- package/dist/delegation/did-key-resolver.d.ts.map +1 -0
- package/dist/delegation/did-key-resolver.js +154 -0
- package/dist/delegation/did-key-resolver.js.map +1 -0
- package/dist/delegation/did-web-resolver.d.ts +83 -0
- package/dist/delegation/did-web-resolver.d.ts.map +1 -0
- package/dist/delegation/did-web-resolver.js +218 -0
- package/dist/delegation/did-web-resolver.js.map +1 -0
- package/dist/delegation/index.d.ts +21 -0
- package/dist/delegation/index.d.ts.map +1 -0
- package/dist/delegation/index.js +21 -0
- package/dist/delegation/index.js.map +1 -0
- package/dist/delegation/outbound-headers.d.ts +81 -0
- package/dist/delegation/outbound-headers.d.ts.map +1 -0
- package/dist/delegation/outbound-headers.js +139 -0
- package/dist/delegation/outbound-headers.js.map +1 -0
- package/dist/delegation/outbound-proof.d.ts +43 -0
- package/dist/delegation/outbound-proof.d.ts.map +1 -0
- package/dist/delegation/outbound-proof.js +52 -0
- package/dist/delegation/outbound-proof.js.map +1 -0
- package/dist/delegation/statuslist-manager.d.ts +44 -0
- package/dist/delegation/statuslist-manager.d.ts.map +1 -0
- package/dist/delegation/statuslist-manager.js +126 -0
- package/dist/delegation/statuslist-manager.js.map +1 -0
- package/dist/delegation/storage/memory-graph-storage.d.ts +70 -0
- package/dist/delegation/storage/memory-graph-storage.d.ts.map +1 -0
- package/dist/delegation/storage/memory-graph-storage.js +145 -0
- package/dist/delegation/storage/memory-graph-storage.js.map +1 -0
- package/dist/delegation/storage/memory-statuslist-storage.d.ts +19 -0
- package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +1 -0
- package/dist/delegation/storage/memory-statuslist-storage.js +33 -0
- package/dist/delegation/storage/memory-statuslist-storage.js.map +1 -0
- package/dist/delegation/utils.d.ts +49 -0
- package/dist/delegation/utils.d.ts.map +1 -0
- package/dist/delegation/utils.js +131 -0
- package/dist/delegation/utils.js.map +1 -0
- package/dist/delegation/vc-issuer.d.ts +56 -0
- package/dist/delegation/vc-issuer.d.ts.map +1 -0
- package/dist/delegation/vc-issuer.js +80 -0
- package/dist/delegation/vc-issuer.js.map +1 -0
- package/dist/delegation/vc-verifier.d.ts +112 -0
- package/dist/delegation/vc-verifier.d.ts.map +1 -0
- package/dist/delegation/vc-verifier.js +280 -0
- package/dist/delegation/vc-verifier.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/index.d.ts +2 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +2 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/logger.d.ts +23 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +82 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/middleware/index.d.ts +7 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/with-mcpi.d.ts +152 -0
- package/dist/middleware/with-mcpi.d.ts.map +1 -0
- package/dist/middleware/with-mcpi.js +472 -0
- package/dist/middleware/with-mcpi.js.map +1 -0
- package/dist/proof/errors.d.ts +49 -0
- package/dist/proof/errors.d.ts.map +1 -0
- package/dist/proof/errors.js +61 -0
- package/dist/proof/errors.js.map +1 -0
- package/dist/proof/generator.d.ts +65 -0
- package/dist/proof/generator.d.ts.map +1 -0
- package/dist/proof/generator.js +163 -0
- package/dist/proof/generator.js.map +1 -0
- package/dist/proof/index.d.ts +4 -0
- package/dist/proof/index.d.ts.map +1 -0
- package/dist/proof/index.js +4 -0
- package/dist/proof/index.js.map +1 -0
- package/dist/proof/verifier.d.ts +108 -0
- package/dist/proof/verifier.d.ts.map +1 -0
- package/dist/proof/verifier.js +299 -0
- package/dist/proof/verifier.js.map +1 -0
- package/dist/providers/base.d.ts +64 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +19 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +3 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/memory.d.ts +33 -0
- package/dist/providers/memory.d.ts.map +1 -0
- package/dist/providers/memory.js +102 -0
- package/dist/providers/memory.js.map +1 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +2 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/manager.d.ts +77 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +251 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/types/protocol.d.ts +320 -0
- package/dist/types/protocol.d.ts.map +1 -0
- package/dist/types/protocol.js +229 -0
- package/dist/types/protocol.js.map +1 -0
- package/dist/utils/base58.d.ts +31 -0
- package/dist/utils/base58.d.ts.map +1 -0
- package/dist/utils/base58.js +104 -0
- package/dist/utils/base58.js.map +1 -0
- package/dist/utils/base64.d.ts +13 -0
- package/dist/utils/base64.d.ts.map +1 -0
- package/dist/utils/base64.js +99 -0
- package/dist/utils/base64.js.map +1 -0
- package/dist/utils/crypto-service.d.ts +37 -0
- package/dist/utils/crypto-service.d.ts.map +1 -0
- package/dist/utils/crypto-service.js +153 -0
- package/dist/utils/crypto-service.js.map +1 -0
- package/dist/utils/did-helpers.d.ts +156 -0
- package/dist/utils/did-helpers.d.ts.map +1 -0
- package/dist/utils/did-helpers.js +193 -0
- package/dist/utils/did-helpers.js.map +1 -0
- package/dist/utils/ed25519-constants.d.ts +18 -0
- package/dist/utils/ed25519-constants.d.ts.map +1 -0
- package/dist/utils/ed25519-constants.js +21 -0
- package/dist/utils/ed25519-constants.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +105 -0
- package/src/__tests__/integration/full-flow.test.ts +362 -0
- package/src/__tests__/providers/base.test.ts +173 -0
- package/src/__tests__/providers/memory.test.ts +332 -0
- package/src/__tests__/utils/mock-providers.ts +319 -0
- package/src/__tests__/utils/node-crypto-provider.ts +93 -0
- package/src/auth/handshake.ts +411 -0
- package/src/auth/index.ts +11 -0
- package/src/auth/types.ts +40 -0
- package/src/delegation/__tests__/audience-validator.test.ts +110 -0
- package/src/delegation/__tests__/bitstring.test.ts +346 -0
- package/src/delegation/__tests__/cascading-revocation.test.ts +624 -0
- package/src/delegation/__tests__/delegation-graph.test.ts +623 -0
- package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
- package/src/delegation/__tests__/did-web-resolver.test.ts +467 -0
- package/src/delegation/__tests__/outbound-headers.test.ts +230 -0
- package/src/delegation/__tests__/outbound-proof.test.ts +179 -0
- package/src/delegation/__tests__/statuslist-manager.test.ts +515 -0
- package/src/delegation/__tests__/utils.test.ts +185 -0
- package/src/delegation/__tests__/vc-issuer.test.ts +487 -0
- package/src/delegation/__tests__/vc-verifier.test.ts +1029 -0
- package/src/delegation/audience-validator.ts +24 -0
- package/src/delegation/bitstring.ts +160 -0
- package/src/delegation/cascading-revocation.ts +224 -0
- package/src/delegation/delegation-graph.ts +143 -0
- package/src/delegation/did-key-resolver.ts +181 -0
- package/src/delegation/did-web-resolver.ts +270 -0
- package/src/delegation/index.ts +33 -0
- package/src/delegation/outbound-headers.ts +193 -0
- package/src/delegation/outbound-proof.ts +90 -0
- package/src/delegation/statuslist-manager.ts +219 -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/memory-graph-storage.ts +178 -0
- package/src/delegation/storage/memory-statuslist-storage.ts +42 -0
- package/src/delegation/utils.ts +189 -0
- package/src/delegation/vc-issuer.ts +137 -0
- package/src/delegation/vc-verifier.ts +440 -0
- package/src/index.ts +264 -0
- package/src/logging/__tests__/logger.test.ts +366 -0
- package/src/logging/index.ts +6 -0
- package/src/logging/logger.ts +91 -0
- package/src/middleware/__tests__/with-mcpi.test.ts +504 -0
- package/src/middleware/index.ts +16 -0
- package/src/middleware/with-mcpi.ts +766 -0
- package/src/proof/__tests__/proof-generator.test.ts +483 -0
- package/src/proof/__tests__/verifier.test.ts +488 -0
- package/src/proof/errors.ts +75 -0
- package/src/proof/generator.ts +255 -0
- package/src/proof/index.ts +22 -0
- package/src/proof/verifier.ts +449 -0
- package/src/providers/base.ts +68 -0
- package/src/providers/index.ts +15 -0
- package/src/providers/memory.ts +130 -0
- package/src/session/__tests__/session-manager.test.ts +342 -0
- package/src/session/index.ts +7 -0
- package/src/session/manager.ts +332 -0
- package/src/types/protocol.ts +596 -0
- package/src/utils/__tests__/base58.test.ts +281 -0
- package/src/utils/__tests__/base64.test.ts +239 -0
- package/src/utils/__tests__/crypto-service.test.ts +530 -0
- package/src/utils/__tests__/did-helpers.test.ts +156 -0
- package/src/utils/base58.ts +115 -0
- package/src/utils/base64.ts +116 -0
- package/src/utils/crypto-service.ts +209 -0
- package/src/utils/did-helpers.ts +210 -0
- package/src/utils/ed25519-constants.ts +23 -0
- package/src/utils/index.ts +9 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js Crypto Provider for Testing
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in crypto module for Ed25519 operations.
|
|
5
|
+
* Only used in tests — not included in the published package.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as crypto from 'node:crypto';
|
|
9
|
+
import { CryptoProvider } from '../../providers/base.js';
|
|
10
|
+
|
|
11
|
+
export class NodeCryptoProvider extends CryptoProvider {
|
|
12
|
+
async sign(data: Uint8Array, privateKeyBase64: string): Promise<Uint8Array> {
|
|
13
|
+
const privateKey = Buffer.from(privateKeyBase64, 'base64');
|
|
14
|
+
|
|
15
|
+
// Handle both raw 32-byte and full 64-byte Ed25519 keys
|
|
16
|
+
const keyBytes =
|
|
17
|
+
privateKey.length === 64 ? privateKey.subarray(0, 32) : privateKey;
|
|
18
|
+
|
|
19
|
+
// Wrap in PKCS8 format for Node.js crypto
|
|
20
|
+
const pkcs8 = Buffer.concat([
|
|
21
|
+
Buffer.from('302e020100300506032b657004220420', 'hex'),
|
|
22
|
+
keyBytes,
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const keyObject = crypto.createPrivateKey({
|
|
26
|
+
key: pkcs8,
|
|
27
|
+
format: 'der',
|
|
28
|
+
type: 'pkcs8',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const signature = crypto.sign(null, Buffer.from(data), keyObject);
|
|
32
|
+
return new Uint8Array(signature);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async verify(
|
|
36
|
+
data: Uint8Array,
|
|
37
|
+
signature: Uint8Array,
|
|
38
|
+
publicKeyBase64: string
|
|
39
|
+
): Promise<boolean> {
|
|
40
|
+
try {
|
|
41
|
+
const publicKey = Buffer.from(publicKeyBase64, 'base64');
|
|
42
|
+
|
|
43
|
+
// Wrap in SPKI format for Node.js crypto
|
|
44
|
+
const spki = Buffer.concat([
|
|
45
|
+
Buffer.from('302a300506032b6570032100', 'hex'),
|
|
46
|
+
publicKey,
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const keyObject = crypto.createPublicKey({
|
|
50
|
+
key: spki,
|
|
51
|
+
format: 'der',
|
|
52
|
+
type: 'spki',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return crypto.verify(
|
|
56
|
+
null,
|
|
57
|
+
Buffer.from(data),
|
|
58
|
+
keyObject,
|
|
59
|
+
Buffer.from(signature)
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async generateKeyPair(): Promise<{ privateKey: string; publicKey: string }> {
|
|
67
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
|
|
68
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
69
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Extract raw keys from DER encoding
|
|
73
|
+
const rawPrivate = (privateKey as Buffer).subarray(16, 48);
|
|
74
|
+
const rawPublic = (publicKey as Buffer).subarray(12, 44);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
privateKey: rawPrivate.toString('base64'),
|
|
78
|
+
publicKey: rawPublic.toString('base64'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async hash(data: Uint8Array): Promise<string> {
|
|
83
|
+
const hex = crypto
|
|
84
|
+
.createHash('sha256')
|
|
85
|
+
.update(Buffer.from(data))
|
|
86
|
+
.digest('hex');
|
|
87
|
+
return `sha256:${hex}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async randomBytes(length: number): Promise<Uint8Array> {
|
|
91
|
+
return new Uint8Array(crypto.randomBytes(length));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization Handshake — Platform-agnostic Protocol Reference
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the MCP-I authorization flow:
|
|
5
|
+
* 1. Check agent reputation (optional)
|
|
6
|
+
* 2. Verify delegation exists
|
|
7
|
+
* 3. Return needs_authorization error if missing
|
|
8
|
+
*
|
|
9
|
+
* Uses only the global fetch API — no Node-specific imports.
|
|
10
|
+
* Safe to run on Node.js, Cloudflare Workers, and any fetch-capable runtime.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
NeedsAuthorizationError,
|
|
15
|
+
AuthorizationDisplay,
|
|
16
|
+
} from '../types/protocol.js';
|
|
17
|
+
import { createNeedsAuthorizationError } from '../types/protocol.js';
|
|
18
|
+
import type { DelegationRecord } from '../types/protocol.js';
|
|
19
|
+
import { logger } from '../logging/index.js';
|
|
20
|
+
import type { DelegationVerifier, VerifyDelegationResult } from './types.js';
|
|
21
|
+
|
|
22
|
+
export type { DelegationVerifier, VerifyDelegationResult };
|
|
23
|
+
|
|
24
|
+
export interface AgentReputation {
|
|
25
|
+
agentDid: string;
|
|
26
|
+
score: number;
|
|
27
|
+
totalInteractions: number;
|
|
28
|
+
successRate: number;
|
|
29
|
+
riskLevel: 'low' | 'medium' | 'high' | 'unknown';
|
|
30
|
+
updatedAt: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AuthHandshakeConfig {
|
|
34
|
+
delegationVerifier: DelegationVerifier;
|
|
35
|
+
resumeTokenStore: ResumeTokenStore;
|
|
36
|
+
reputationService?: {
|
|
37
|
+
apiUrl: string;
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
apiFormat?: 'v1' | 'v2';
|
|
40
|
+
};
|
|
41
|
+
authorization: {
|
|
42
|
+
authorizationUrl: string;
|
|
43
|
+
resumeTokenTtl?: number;
|
|
44
|
+
requireAuthForUnknown?: boolean;
|
|
45
|
+
minReputationScore?: number;
|
|
46
|
+
};
|
|
47
|
+
debug?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface VerifyOrHintsResult {
|
|
51
|
+
authorized: boolean;
|
|
52
|
+
delegation?: DelegationRecord;
|
|
53
|
+
credential?: {
|
|
54
|
+
agent_did: string;
|
|
55
|
+
user_did: string;
|
|
56
|
+
scopes: string[];
|
|
57
|
+
authorization: {
|
|
58
|
+
type:
|
|
59
|
+
| 'oauth'
|
|
60
|
+
| 'oauth2'
|
|
61
|
+
| 'password'
|
|
62
|
+
| 'credential'
|
|
63
|
+
| 'webauthn'
|
|
64
|
+
| 'siwe'
|
|
65
|
+
| 'none';
|
|
66
|
+
provider?: string;
|
|
67
|
+
credentialType?: string;
|
|
68
|
+
rpId?: string;
|
|
69
|
+
userVerification?: 'required' | 'preferred' | 'discouraged';
|
|
70
|
+
chainId?: number;
|
|
71
|
+
domain?: string;
|
|
72
|
+
};
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
};
|
|
75
|
+
authError?: NeedsAuthorizationError;
|
|
76
|
+
reputation?: AgentReputation;
|
|
77
|
+
reason?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ResumeTokenStore {
|
|
81
|
+
create(
|
|
82
|
+
agentDid: string,
|
|
83
|
+
scopes: string[],
|
|
84
|
+
metadata?: Record<string, unknown>
|
|
85
|
+
): Promise<string>;
|
|
86
|
+
|
|
87
|
+
get(token: string): Promise<{
|
|
88
|
+
agentDid: string;
|
|
89
|
+
scopes: string[];
|
|
90
|
+
createdAt: number;
|
|
91
|
+
expiresAt: number;
|
|
92
|
+
metadata?: Record<string, unknown>;
|
|
93
|
+
} | null>;
|
|
94
|
+
|
|
95
|
+
fulfill(token: string): Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class MemoryResumeTokenStore implements ResumeTokenStore {
|
|
99
|
+
private tokens = new Map<
|
|
100
|
+
string,
|
|
101
|
+
{
|
|
102
|
+
agentDid: string;
|
|
103
|
+
scopes: string[];
|
|
104
|
+
createdAt: number;
|
|
105
|
+
expiresAt: number;
|
|
106
|
+
metadata?: Record<string, unknown>;
|
|
107
|
+
fulfilled: boolean;
|
|
108
|
+
}
|
|
109
|
+
>();
|
|
110
|
+
private ttl: number;
|
|
111
|
+
|
|
112
|
+
constructor(ttlMs = 600_000) {
|
|
113
|
+
this.ttl = ttlMs;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async create(
|
|
117
|
+
agentDid: string,
|
|
118
|
+
scopes: string[],
|
|
119
|
+
metadata?: Record<string, unknown>
|
|
120
|
+
): Promise<string> {
|
|
121
|
+
const token = `rt_${Date.now()}_${Math.random().toString(36).substring(2, 18)}`;
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
|
|
124
|
+
this.tokens.set(token, {
|
|
125
|
+
agentDid,
|
|
126
|
+
scopes,
|
|
127
|
+
createdAt: now,
|
|
128
|
+
expiresAt: now + this.ttl,
|
|
129
|
+
metadata,
|
|
130
|
+
fulfilled: false,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return token;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async get(token: string): Promise<{
|
|
137
|
+
agentDid: string;
|
|
138
|
+
scopes: string[];
|
|
139
|
+
createdAt: number;
|
|
140
|
+
expiresAt: number;
|
|
141
|
+
metadata?: Record<string, unknown>;
|
|
142
|
+
} | null> {
|
|
143
|
+
const data = this.tokens.get(token);
|
|
144
|
+
if (!data) return null;
|
|
145
|
+
|
|
146
|
+
if (Date.now() > data.expiresAt) {
|
|
147
|
+
this.tokens.delete(token);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (data.fulfilled) return null;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
agentDid: data.agentDid,
|
|
155
|
+
scopes: data.scopes,
|
|
156
|
+
createdAt: data.createdAt,
|
|
157
|
+
expiresAt: data.expiresAt,
|
|
158
|
+
metadata: data.metadata,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async fulfill(token: string): Promise<void> {
|
|
163
|
+
const data = this.tokens.get(token);
|
|
164
|
+
if (data) {
|
|
165
|
+
data.fulfilled = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
clear(): void {
|
|
170
|
+
this.tokens.clear();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Verify agent delegation or return authorization hints.
|
|
176
|
+
*
|
|
177
|
+
* Orchestrates the authorization flow:
|
|
178
|
+
* 1. Optionally check agent reputation against threshold
|
|
179
|
+
* 2. Verify existing delegation via DelegationVerifier
|
|
180
|
+
* 3. Return authorization hints if delegation is missing/invalid
|
|
181
|
+
*
|
|
182
|
+
* @param agentDid - The agent's DID to verify
|
|
183
|
+
* @param scopes - Required scopes for the operation
|
|
184
|
+
* @param config - Authorization configuration including verifier, token store, etc.
|
|
185
|
+
* @param _resumeToken - Optional resume token from previous authorization attempt
|
|
186
|
+
* @returns Result indicating authorization status, delegation, or auth hints
|
|
187
|
+
*/
|
|
188
|
+
export async function verifyOrHints(
|
|
189
|
+
agentDid: string,
|
|
190
|
+
scopes: string[],
|
|
191
|
+
config: AuthHandshakeConfig,
|
|
192
|
+
_resumeToken?: string
|
|
193
|
+
): Promise<VerifyOrHintsResult> {
|
|
194
|
+
const startTime = Date.now();
|
|
195
|
+
|
|
196
|
+
if (config.debug) {
|
|
197
|
+
logger.debug(`[AuthHandshake] Verifying ${agentDid} for scopes: ${scopes.join(', ')}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let reputation: AgentReputation | undefined;
|
|
201
|
+
if (config.reputationService && config.authorization.minReputationScore !== undefined) {
|
|
202
|
+
try {
|
|
203
|
+
reputation = await fetchAgentReputation(agentDid, config.reputationService);
|
|
204
|
+
|
|
205
|
+
if (config.debug) {
|
|
206
|
+
logger.debug(`[AuthHandshake] Reputation score: ${reputation.score}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (reputation.score < config.authorization.minReputationScore) {
|
|
210
|
+
if (config.debug) {
|
|
211
|
+
logger.debug(
|
|
212
|
+
`[AuthHandshake] Reputation ${reputation.score} < ${config.authorization.minReputationScore}, requiring authorization`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const authError = await buildNeedsAuthorizationError(
|
|
217
|
+
agentDid,
|
|
218
|
+
scopes,
|
|
219
|
+
config,
|
|
220
|
+
'Agent reputation score below threshold'
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
authorized: false,
|
|
225
|
+
authError,
|
|
226
|
+
reputation,
|
|
227
|
+
reason: 'Low reputation score',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.warn('[AuthHandshake] Failed to check reputation:', error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let delegationResult: VerifyDelegationResult;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
delegationResult = await config.delegationVerifier.verify(agentDid, scopes);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
logger.error('[AuthHandshake] Delegation verification failed:', error);
|
|
241
|
+
const errorMessage = `Delegation verification error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
242
|
+
|
|
243
|
+
const authError = await buildNeedsAuthorizationError(agentDid, scopes, config, errorMessage);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
authorized: false,
|
|
247
|
+
authError,
|
|
248
|
+
reason: errorMessage,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (delegationResult.valid && delegationResult.delegation) {
|
|
253
|
+
if (config.debug) {
|
|
254
|
+
logger.debug(
|
|
255
|
+
`[AuthHandshake] Delegation valid, authorized (${Date.now() - startTime}ms)`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
authorized: true,
|
|
261
|
+
delegation: delegationResult.delegation,
|
|
262
|
+
credential: delegationResult.credential,
|
|
263
|
+
reputation,
|
|
264
|
+
reason: 'Valid delegation found',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (config.debug) {
|
|
269
|
+
logger.debug(
|
|
270
|
+
`[AuthHandshake] No delegation found, returning needs_authorization (${Date.now() - startTime}ms)`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const authError = await buildNeedsAuthorizationError(
|
|
275
|
+
agentDid,
|
|
276
|
+
scopes,
|
|
277
|
+
config,
|
|
278
|
+
delegationResult.reason ?? 'No valid delegation found'
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
authorized: false,
|
|
283
|
+
authError,
|
|
284
|
+
reputation,
|
|
285
|
+
reason: delegationResult.reason ?? 'No delegation',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function fetchAgentReputation(
|
|
290
|
+
agentDid: string,
|
|
291
|
+
reputationConfig: { apiUrl: string; apiKey?: string; apiFormat?: 'v1' | 'v2' }
|
|
292
|
+
): Promise<AgentReputation> {
|
|
293
|
+
const apiUrl = reputationConfig.apiUrl.replace(/\/$/, '');
|
|
294
|
+
const headers: Record<string, string> = {
|
|
295
|
+
'Content-Type': 'application/json',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
if (reputationConfig.apiKey) {
|
|
299
|
+
headers['X-API-Key'] = reputationConfig.apiKey;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const isV2Format = reputationConfig.apiFormat === 'v2';
|
|
303
|
+
let response: Response;
|
|
304
|
+
|
|
305
|
+
if (isV2Format) {
|
|
306
|
+
response = await fetch(
|
|
307
|
+
`${apiUrl}/v1/reputation/${encodeURIComponent(agentDid)}`,
|
|
308
|
+
{
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers,
|
|
311
|
+
body: JSON.stringify({ include_details: false }),
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
response = await fetch(
|
|
316
|
+
`${apiUrl}/api/v1/reputation/${encodeURIComponent(agentDid)}`,
|
|
317
|
+
{ method: 'GET', headers }
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
if (response.status === 404) {
|
|
323
|
+
return {
|
|
324
|
+
agentDid,
|
|
325
|
+
score: 50,
|
|
326
|
+
totalInteractions: 0,
|
|
327
|
+
successRate: 0,
|
|
328
|
+
riskLevel: 'unknown',
|
|
329
|
+
updatedAt: Date.now(),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
throw new Error(`Reputation API error: ${response.status} ${response.statusText}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
336
|
+
|
|
337
|
+
const score = (data['score'] as number | undefined) ?? 50;
|
|
338
|
+
const levelRaw = (
|
|
339
|
+
(data['level'] as string | undefined) ??
|
|
340
|
+
(data['riskLevel'] as string | undefined) ??
|
|
341
|
+
'unknown'
|
|
342
|
+
).toLowerCase();
|
|
343
|
+
const riskLevel: AgentReputation['riskLevel'] =
|
|
344
|
+
levelRaw === 'low' || levelRaw === 'medium' || levelRaw === 'high' ? levelRaw : 'unknown';
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
agentDid:
|
|
348
|
+
(data['agent_did'] as string | undefined) ??
|
|
349
|
+
(data['agentDid'] as string | undefined) ??
|
|
350
|
+
agentDid,
|
|
351
|
+
score,
|
|
352
|
+
totalInteractions: (data['totalInteractions'] as number | undefined) ?? 0,
|
|
353
|
+
successRate: (data['successRate'] as number | undefined) ?? 0,
|
|
354
|
+
riskLevel,
|
|
355
|
+
updatedAt: data['calculatedAt']
|
|
356
|
+
? new Date(data['calculatedAt'] as string).getTime()
|
|
357
|
+
: ((data['updatedAt'] as number | undefined) ?? Date.now()),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function buildNeedsAuthorizationError(
|
|
362
|
+
agentDid: string,
|
|
363
|
+
scopes: string[],
|
|
364
|
+
config: AuthHandshakeConfig,
|
|
365
|
+
message: string
|
|
366
|
+
): Promise<NeedsAuthorizationError> {
|
|
367
|
+
const resumeToken = await config.resumeTokenStore.create(agentDid, scopes, {
|
|
368
|
+
requestedAt: Date.now(),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const expiresAt = Date.now() + (config.authorization.resumeTokenTtl ?? 600_000);
|
|
372
|
+
|
|
373
|
+
const authUrl = new URL(config.authorization.authorizationUrl);
|
|
374
|
+
authUrl.searchParams.set('agent_did', agentDid);
|
|
375
|
+
authUrl.searchParams.set('scopes', scopes.join(','));
|
|
376
|
+
authUrl.searchParams.set('resume_token', resumeToken);
|
|
377
|
+
|
|
378
|
+
const authCode = resumeToken.substring(0, 8).toUpperCase();
|
|
379
|
+
|
|
380
|
+
const display: AuthorizationDisplay = {
|
|
381
|
+
title: 'Authorization Required',
|
|
382
|
+
hint: ['link', 'qr'],
|
|
383
|
+
authorizationCode: authCode,
|
|
384
|
+
qrUrl: `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(authUrl.toString())}`,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return createNeedsAuthorizationError({
|
|
388
|
+
message,
|
|
389
|
+
authorizationUrl: authUrl.toString(),
|
|
390
|
+
resumeToken,
|
|
391
|
+
expiresAt,
|
|
392
|
+
scopes,
|
|
393
|
+
display,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function hasSensitiveScopes(scopes: string[]): boolean {
|
|
398
|
+
const sensitivePatterns = [
|
|
399
|
+
'write',
|
|
400
|
+
'delete',
|
|
401
|
+
'admin',
|
|
402
|
+
'payment',
|
|
403
|
+
'transfer',
|
|
404
|
+
'execute',
|
|
405
|
+
'modify',
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
return scopes.some((scope) =>
|
|
409
|
+
sensitivePatterns.some((pattern) => scope.toLowerCase().includes(pattern))
|
|
410
|
+
);
|
|
411
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
verifyOrHints,
|
|
3
|
+
hasSensitiveScopes,
|
|
4
|
+
MemoryResumeTokenStore,
|
|
5
|
+
type AuthHandshakeConfig,
|
|
6
|
+
type VerifyOrHintsResult,
|
|
7
|
+
type AgentReputation,
|
|
8
|
+
type ResumeTokenStore,
|
|
9
|
+
} from './handshake.js';
|
|
10
|
+
|
|
11
|
+
export type { DelegationVerifier, VerifyDelegationResult } from './types.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization types for the auth module.
|
|
3
|
+
*
|
|
4
|
+
* Minimal interfaces required by the auth handshake.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DelegationRecord } from '../types/protocol.js';
|
|
8
|
+
|
|
9
|
+
export interface VerifyDelegationResult {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
delegation?: DelegationRecord;
|
|
12
|
+
credential?: {
|
|
13
|
+
agent_did: string;
|
|
14
|
+
user_did: string;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
authorization: {
|
|
17
|
+
type:
|
|
18
|
+
| 'oauth'
|
|
19
|
+
| 'oauth2'
|
|
20
|
+
| 'password'
|
|
21
|
+
| 'credential'
|
|
22
|
+
| 'webauthn'
|
|
23
|
+
| 'siwe'
|
|
24
|
+
| 'none';
|
|
25
|
+
provider?: string;
|
|
26
|
+
credentialType?: string;
|
|
27
|
+
rpId?: string;
|
|
28
|
+
userVerification?: 'required' | 'preferred' | 'discouraged';
|
|
29
|
+
chainId?: number;
|
|
30
|
+
domain?: string;
|
|
31
|
+
};
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
reason?: string;
|
|
35
|
+
cached?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DelegationVerifier {
|
|
39
|
+
verify(agentDid: string, scopes: string[]): Promise<VerifyDelegationResult>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Delegation Audience Validation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { verifyDelegationAudience } from "../audience-validator.js";
|
|
7
|
+
import type { DelegationRecord } from "../../types/protocol.js";
|
|
8
|
+
|
|
9
|
+
describe("verifyDelegationAudience", () => {
|
|
10
|
+
const serverDid = "did:web:server.example.com";
|
|
11
|
+
|
|
12
|
+
it("should return true when delegation has no audience", () => {
|
|
13
|
+
const delegation: DelegationRecord = {
|
|
14
|
+
id: "del_001",
|
|
15
|
+
issuerDid: "did:web:user.com",
|
|
16
|
+
subjectDid: "did:key:zagent123",
|
|
17
|
+
controller: "user_alice",
|
|
18
|
+
vcId: "vc_001",
|
|
19
|
+
constraints: {
|
|
20
|
+
scopes: ["tool:execute"],
|
|
21
|
+
// No audience field
|
|
22
|
+
},
|
|
23
|
+
createdAt: Date.now(),
|
|
24
|
+
expiresAt: Date.now() + 3600000,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
expect(verifyDelegationAudience(delegation, serverDid)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should return true when delegation audience matches server DID", () => {
|
|
31
|
+
const delegation: DelegationRecord = {
|
|
32
|
+
id: "del_002",
|
|
33
|
+
issuerDid: "did:web:user.com",
|
|
34
|
+
subjectDid: "did:key:zagent123",
|
|
35
|
+
controller: "user_bob",
|
|
36
|
+
vcId: "vc_002",
|
|
37
|
+
constraints: {
|
|
38
|
+
scopes: ["tool:execute"],
|
|
39
|
+
audience: serverDid, // Matches server DID
|
|
40
|
+
},
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
expiresAt: Date.now() + 3600000,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(verifyDelegationAudience(delegation, serverDid)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return false when delegation audience does not match server DID", () => {
|
|
49
|
+
const delegation: DelegationRecord = {
|
|
50
|
+
id: "del_003",
|
|
51
|
+
issuerDid: "did:web:user.com",
|
|
52
|
+
subjectDid: "did:key:zagent123",
|
|
53
|
+
controller: "user_charlie",
|
|
54
|
+
vcId: "vc_003",
|
|
55
|
+
constraints: {
|
|
56
|
+
scopes: ["tool:execute"],
|
|
57
|
+
audience: "did:web:other-server.com", // Different server
|
|
58
|
+
},
|
|
59
|
+
createdAt: Date.now(),
|
|
60
|
+
expiresAt: Date.now() + 3600000,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
expect(verifyDelegationAudience(delegation, serverDid)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return true when server DID is in audience array", () => {
|
|
67
|
+
const delegation: DelegationRecord = {
|
|
68
|
+
id: "del_004",
|
|
69
|
+
issuerDid: "did:web:user.com",
|
|
70
|
+
subjectDid: "did:key:zagent123",
|
|
71
|
+
controller: "user_dave",
|
|
72
|
+
vcId: "vc_004",
|
|
73
|
+
constraints: {
|
|
74
|
+
scopes: ["tool:execute"],
|
|
75
|
+
audience: [
|
|
76
|
+
"did:web:server1.com",
|
|
77
|
+
serverDid, // Server DID is in array
|
|
78
|
+
"did:web:server3.com",
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
createdAt: Date.now(),
|
|
82
|
+
expiresAt: Date.now() + 3600000,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
expect(verifyDelegationAudience(delegation, serverDid)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return false when server DID is not in audience array", () => {
|
|
89
|
+
const delegation: DelegationRecord = {
|
|
90
|
+
id: "del_005",
|
|
91
|
+
issuerDid: "did:web:user.com",
|
|
92
|
+
subjectDid: "did:key:zagent123",
|
|
93
|
+
controller: "user_eve",
|
|
94
|
+
vcId: "vc_005",
|
|
95
|
+
constraints: {
|
|
96
|
+
scopes: ["tool:execute"],
|
|
97
|
+
audience: [
|
|
98
|
+
"did:web:server1.com",
|
|
99
|
+
"did:web:server2.com",
|
|
100
|
+
// serverDid not in array
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
createdAt: Date.now(),
|
|
104
|
+
expiresAt: Date.now() + 3600000,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
expect(verifyDelegationAudience(delegation, serverDid)).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|