@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,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager Tests — @mcp-i/core
|
|
3
|
+
*
|
|
4
|
+
* Verifies platform-agnostic SessionManager behaviour:
|
|
5
|
+
* - Nonce format identical to existing implementation
|
|
6
|
+
* - TTL / expiry behaviour unchanged
|
|
7
|
+
* - getSession returns correct session by ID
|
|
8
|
+
* - validateHandshake behaviour unchanged
|
|
9
|
+
*
|
|
10
|
+
* NodeCryptoProvider is injected (test environment is Node.js).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
14
|
+
import { SessionManager, createHandshakeRequest, validateHandshakeFormat } from "../manager.js";
|
|
15
|
+
import type { SessionConfig } from "../manager.js";
|
|
16
|
+
import type { HandshakeRequest } from "../../types/protocol.js";
|
|
17
|
+
|
|
18
|
+
// NodeCryptoProvider for test environment (Node.js)
|
|
19
|
+
import { NodeCryptoProvider } from "../../__tests__/utils/node-crypto-provider.js";
|
|
20
|
+
|
|
21
|
+
const cryptoProvider = new NodeCryptoProvider();
|
|
22
|
+
|
|
23
|
+
function makeSessionManager(config: SessionConfig = {}): SessionManager {
|
|
24
|
+
return new SessionManager(cryptoProvider, config);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeRequest(overrides: Partial<HandshakeRequest> = {}): HandshakeRequest {
|
|
28
|
+
return {
|
|
29
|
+
nonce: SessionManager.generateNonce(),
|
|
30
|
+
audience: "example.com",
|
|
31
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
32
|
+
agentDid: "did:key:zAgent",
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("SessionManager", () => {
|
|
38
|
+
let manager: SessionManager;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
manager = makeSessionManager();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("Nonce format", () => {
|
|
45
|
+
it("should generate nonce as base64url string", () => {
|
|
46
|
+
const nonce = SessionManager.generateNonce();
|
|
47
|
+
expect(typeof nonce).toBe("string");
|
|
48
|
+
expect(nonce.length).toBeGreaterThan(0);
|
|
49
|
+
// base64url uses A-Z a-z 0-9 - _ (no padding)
|
|
50
|
+
expect(nonce).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should generate unique nonces", () => {
|
|
54
|
+
const nonces = new Set(Array.from({ length: 20 }, () => SessionManager.generateNonce()));
|
|
55
|
+
expect(nonces.size).toBe(20);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should generate nonces of consistent entropy (16-byte = ~22 base64url chars)", () => {
|
|
59
|
+
const nonce = SessionManager.generateNonce();
|
|
60
|
+
// 16 bytes base64url → ceil(16 * 4/3) = 22 chars (no padding)
|
|
61
|
+
expect(nonce.length).toBeGreaterThanOrEqual(20);
|
|
62
|
+
expect(nonce.length).toBeLessThanOrEqual(24);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("Handshake validation", () => {
|
|
67
|
+
it("should create a valid session on correct handshake", async () => {
|
|
68
|
+
const request = makeRequest();
|
|
69
|
+
const result = await manager.validateHandshake(request);
|
|
70
|
+
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
expect(result.session).toBeDefined();
|
|
73
|
+
expect(result.session?.audience).toBe(request.audience);
|
|
74
|
+
expect(result.session?.nonce).toBe(request.nonce);
|
|
75
|
+
expect(result.session?.identityState).toBe("anonymous");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should return session ID with mcpi_ prefix", async () => {
|
|
79
|
+
const request = makeRequest();
|
|
80
|
+
const result = await manager.validateHandshake(request);
|
|
81
|
+
|
|
82
|
+
expect(result.session?.sessionId).toMatch(/^mcpi_/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should reject request with stale timestamp", async () => {
|
|
86
|
+
const staleTs = Math.floor(Date.now() / 1000) - 200; // 200s ago, skew is 120s
|
|
87
|
+
const request = makeRequest({ timestamp: staleTs });
|
|
88
|
+
const result = await manager.validateHandshake(request);
|
|
89
|
+
|
|
90
|
+
expect(result.success).toBe(false);
|
|
91
|
+
expect(result.error?.code).toBe("XMCP_I_EHANDSHAKE");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should accept request within timestamp skew", async () => {
|
|
95
|
+
const slightlyOldTs = Math.floor(Date.now() / 1000) - 60; // 60s ago, within 120s default
|
|
96
|
+
const request = makeRequest({ timestamp: slightlyOldTs });
|
|
97
|
+
const result = await manager.validateHandshake(request);
|
|
98
|
+
|
|
99
|
+
expect(result.success).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should reject replayed nonce", async () => {
|
|
103
|
+
const request = makeRequest();
|
|
104
|
+
await manager.validateHandshake(request);
|
|
105
|
+
|
|
106
|
+
// Same request again (same nonce)
|
|
107
|
+
const second = await manager.validateHandshake(request);
|
|
108
|
+
expect(second.success).toBe(false);
|
|
109
|
+
expect(second.error?.code).toBe("XMCP_I_EHANDSHAKE");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should accept different nonces in succession", async () => {
|
|
113
|
+
const r1 = makeRequest();
|
|
114
|
+
const r2 = makeRequest(); // fresh nonce
|
|
115
|
+
|
|
116
|
+
const res1 = await manager.validateHandshake(r1);
|
|
117
|
+
const res2 = await manager.validateHandshake(r2);
|
|
118
|
+
|
|
119
|
+
expect(res1.success).toBe(true);
|
|
120
|
+
expect(res2.success).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("Session lookup — getSession", () => {
|
|
125
|
+
it("should return session by ID after handshake", async () => {
|
|
126
|
+
const request = makeRequest();
|
|
127
|
+
const { session } = await manager.validateHandshake(request);
|
|
128
|
+
expect(session).toBeDefined();
|
|
129
|
+
|
|
130
|
+
const found = await manager.getSession(session!.sessionId);
|
|
131
|
+
expect(found).toBeDefined();
|
|
132
|
+
expect(found?.sessionId).toBe(session!.sessionId);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should return null for unknown session ID", async () => {
|
|
136
|
+
const found = await manager.getSession("mcpi_does-not-exist");
|
|
137
|
+
expect(found).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should update lastActivity on each getSession call", async () => {
|
|
141
|
+
const request = makeRequest();
|
|
142
|
+
const { session } = await manager.validateHandshake(request);
|
|
143
|
+
|
|
144
|
+
const before = session!.lastActivity;
|
|
145
|
+
// Artificially advance lastActivity to simulate time passing
|
|
146
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
147
|
+
const found = await manager.getSession(session!.sessionId);
|
|
148
|
+
|
|
149
|
+
// lastActivity should be updated (>= before)
|
|
150
|
+
expect(found!.lastActivity).toBeGreaterThanOrEqual(before);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("Session expiry — TTL behaviour", () => {
|
|
155
|
+
it("should expire idle sessions after TTL", async () => {
|
|
156
|
+
// Use 1-minute TTL but manually manipulate lastActivity
|
|
157
|
+
const sm = makeSessionManager({ sessionTtlMinutes: 1 });
|
|
158
|
+
const request = makeRequest();
|
|
159
|
+
const { session } = await sm.validateHandshake(request);
|
|
160
|
+
expect(session).toBeDefined();
|
|
161
|
+
|
|
162
|
+
// Backdate lastActivity beyond TTL (simulate 70s of idle)
|
|
163
|
+
session!.lastActivity = Math.floor(Date.now() / 1000) - 70;
|
|
164
|
+
// Directly set in sessions map via cleanup
|
|
165
|
+
await sm.cleanup();
|
|
166
|
+
|
|
167
|
+
const found = await sm.getSession(session!.sessionId);
|
|
168
|
+
expect(found).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should not expire active sessions within TTL", async () => {
|
|
172
|
+
const sm = makeSessionManager({ sessionTtlMinutes: 30 });
|
|
173
|
+
const request = makeRequest();
|
|
174
|
+
const { session } = await sm.validateHandshake(request);
|
|
175
|
+
expect(session).toBeDefined();
|
|
176
|
+
|
|
177
|
+
const found = await sm.getSession(session!.sessionId);
|
|
178
|
+
expect(found).not.toBeNull();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should expire sessions beyond absolute lifetime", async () => {
|
|
182
|
+
const sm = makeSessionManager({ absoluteSessionLifetime: 1 });
|
|
183
|
+
const request = makeRequest();
|
|
184
|
+
const { session } = await sm.validateHandshake(request);
|
|
185
|
+
expect(session).toBeDefined();
|
|
186
|
+
|
|
187
|
+
// Backdate createdAt beyond 1-minute absolute lifetime
|
|
188
|
+
session!.createdAt = Math.floor(Date.now() / 1000) - 65;
|
|
189
|
+
await sm.cleanup();
|
|
190
|
+
|
|
191
|
+
const found = await sm.getSession(session!.sessionId);
|
|
192
|
+
expect(found).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("Custom timestamp skew", () => {
|
|
197
|
+
it("should use custom timestampSkewSeconds when provided", async () => {
|
|
198
|
+
const sm = makeSessionManager({ timestampSkewSeconds: 30 });
|
|
199
|
+
|
|
200
|
+
// 40s stale — should fail with 30s skew
|
|
201
|
+
const staleRequest = makeRequest({
|
|
202
|
+
timestamp: Math.floor(Date.now() / 1000) - 40,
|
|
203
|
+
});
|
|
204
|
+
const result = await sm.validateHandshake(staleRequest);
|
|
205
|
+
|
|
206
|
+
expect(result.success).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should accept request within custom skew", async () => {
|
|
210
|
+
const sm = makeSessionManager({ timestampSkewSeconds: 60 });
|
|
211
|
+
|
|
212
|
+
// 50s stale — should pass with 60s skew
|
|
213
|
+
const request = makeRequest({
|
|
214
|
+
timestamp: Math.floor(Date.now() / 1000) - 50,
|
|
215
|
+
});
|
|
216
|
+
const result = await sm.validateHandshake(request);
|
|
217
|
+
|
|
218
|
+
expect(result.success).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("getStats", () => {
|
|
223
|
+
it("should report zero active sessions initially", () => {
|
|
224
|
+
const stats = manager.getStats();
|
|
225
|
+
expect(stats.activeSessions).toBe(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should report correct count after handshakes", async () => {
|
|
229
|
+
await manager.validateHandshake(makeRequest());
|
|
230
|
+
await manager.validateHandshake(makeRequest());
|
|
231
|
+
|
|
232
|
+
const stats = manager.getStats();
|
|
233
|
+
expect(stats.activeSessions).toBe(2);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("clearSessions", () => {
|
|
238
|
+
it("should clear all sessions", async () => {
|
|
239
|
+
await manager.validateHandshake(makeRequest());
|
|
240
|
+
manager.clearSessions();
|
|
241
|
+
|
|
242
|
+
const stats = manager.getStats();
|
|
243
|
+
expect(stats.activeSessions).toBe(0);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("setServerDid", () => {
|
|
248
|
+
it("should include serverDid in session when set", async () => {
|
|
249
|
+
manager.setServerDid("did:web:example.com:server");
|
|
250
|
+
const request = makeRequest({ audience: "did:web:example.com:server" });
|
|
251
|
+
const { session } = await manager.validateHandshake(request);
|
|
252
|
+
|
|
253
|
+
expect(session?.serverDid).toBe("did:web:example.com:server");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("Audience validation", () => {
|
|
258
|
+
it("should reject handshake when audience doesn't match serverDid", async () => {
|
|
259
|
+
const sm = makeSessionManager({ serverDid: "did:web:example.com:server" });
|
|
260
|
+
const request = makeRequest({ audience: "did:web:other.com:server" });
|
|
261
|
+
const result = await sm.validateHandshake(request);
|
|
262
|
+
|
|
263
|
+
expect(result.success).toBe(false);
|
|
264
|
+
expect(result.error?.code).toBe("MCPI_AUDIENCE_MISMATCH");
|
|
265
|
+
expect(result.error?.message).toContain("Audience mismatch");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should accept handshake when audience matches serverDid", async () => {
|
|
269
|
+
const sm = makeSessionManager({ serverDid: "did:web:example.com:server" });
|
|
270
|
+
const request = makeRequest({ audience: "did:web:example.com:server" });
|
|
271
|
+
const result = await sm.validateHandshake(request);
|
|
272
|
+
|
|
273
|
+
expect(result.success).toBe(true);
|
|
274
|
+
expect(result.session?.audience).toBe("did:web:example.com:server");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should accept handshake when serverDid is not configured (backward compat)", async () => {
|
|
278
|
+
const sm = makeSessionManager(); // No serverDid configured
|
|
279
|
+
const request = makeRequest({ audience: "any-audience" });
|
|
280
|
+
const result = await sm.validateHandshake(request);
|
|
281
|
+
|
|
282
|
+
expect(result.success).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("createHandshakeRequest", () => {
|
|
288
|
+
it("should create a valid handshake request for an audience", () => {
|
|
289
|
+
const req = createHandshakeRequest("example.com");
|
|
290
|
+
|
|
291
|
+
expect(req.audience).toBe("example.com");
|
|
292
|
+
expect(typeof req.nonce).toBe("string");
|
|
293
|
+
expect(req.nonce.length).toBeGreaterThan(0);
|
|
294
|
+
expect(typeof req.timestamp).toBe("number");
|
|
295
|
+
expect(req.timestamp).toBeGreaterThan(0);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should generate unique nonces across calls", () => {
|
|
299
|
+
const nonces = new Set(
|
|
300
|
+
Array.from({ length: 20 }, () => createHandshakeRequest("test.com").nonce)
|
|
301
|
+
);
|
|
302
|
+
expect(nonces.size).toBe(20);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should use current timestamp", () => {
|
|
306
|
+
const before = Math.floor(Date.now() / 1000);
|
|
307
|
+
const req = createHandshakeRequest("test.com");
|
|
308
|
+
const after = Math.floor(Date.now() / 1000);
|
|
309
|
+
|
|
310
|
+
expect(req.timestamp).toBeGreaterThanOrEqual(before);
|
|
311
|
+
expect(req.timestamp).toBeLessThanOrEqual(after);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("validateHandshakeFormat", () => {
|
|
316
|
+
it("should return true for a valid request", () => {
|
|
317
|
+
const req = createHandshakeRequest("example.com");
|
|
318
|
+
expect(validateHandshakeFormat(req)).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should return false for missing nonce", () => {
|
|
322
|
+
expect(validateHandshakeFormat({ audience: "x", timestamp: 1 })).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("should return false for empty nonce", () => {
|
|
326
|
+
expect(validateHandshakeFormat({ nonce: "", audience: "x", timestamp: 1 })).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should return false for missing audience", () => {
|
|
330
|
+
expect(validateHandshakeFormat({ nonce: "abc", timestamp: 1 })).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should return false for non-integer timestamp", () => {
|
|
334
|
+
expect(validateHandshakeFormat({ nonce: "abc", audience: "x", timestamp: 1.5 })).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should return false for non-object", () => {
|
|
338
|
+
expect(validateHandshakeFormat(null)).toBe(false);
|
|
339
|
+
expect(validateHandshakeFormat("string")).toBe(false);
|
|
340
|
+
expect(validateHandshakeFormat(42)).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Management — Platform-agnostic Protocol Reference
|
|
3
|
+
*
|
|
4
|
+
* Handles handshake enforcement, session management, and nonce validation
|
|
5
|
+
* according to MCP-I requirements 4.5–4.9 and 19.1–19.2.
|
|
6
|
+
*
|
|
7
|
+
* Platform adapters inject a CryptoProvider for all random byte generation.
|
|
8
|
+
* The static generateNonce() uses globalThis.crypto (available Node 20+ and
|
|
9
|
+
* Cloudflare Workers) to remain synchronous without platform-specific imports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
HandshakeRequest,
|
|
14
|
+
SessionContext,
|
|
15
|
+
NonceCache,
|
|
16
|
+
} from '../types/protocol.js';
|
|
17
|
+
import type { CryptoProvider } from '../providers/base.js';
|
|
18
|
+
import { MemoryNonceCacheProvider } from '../providers/memory.js';
|
|
19
|
+
import { logger } from '../logging/index.js';
|
|
20
|
+
|
|
21
|
+
export interface SessionConfig {
|
|
22
|
+
timestampSkewSeconds?: number;
|
|
23
|
+
sessionTtlMinutes?: number;
|
|
24
|
+
absoluteSessionLifetime?: number;
|
|
25
|
+
nonceCache?: NonceCache;
|
|
26
|
+
serverDid?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HandshakeResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
session?: SessionContext;
|
|
32
|
+
error?: {
|
|
33
|
+
code: string;
|
|
34
|
+
message: string;
|
|
35
|
+
remediation?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class SessionManager {
|
|
40
|
+
private config: Required<Omit<SessionConfig, 'absoluteSessionLifetime' | 'serverDid'>> & {
|
|
41
|
+
absoluteSessionLifetime?: number;
|
|
42
|
+
serverDid?: string;
|
|
43
|
+
};
|
|
44
|
+
private cryptoProvider: CryptoProvider;
|
|
45
|
+
private sessions = new Map<string, SessionContext>();
|
|
46
|
+
|
|
47
|
+
constructor(cryptoProvider: CryptoProvider, config: SessionConfig = {}) {
|
|
48
|
+
this.cryptoProvider = cryptoProvider;
|
|
49
|
+
this.config = {
|
|
50
|
+
timestampSkewSeconds: config.timestampSkewSeconds ?? 120,
|
|
51
|
+
sessionTtlMinutes: config.sessionTtlMinutes ?? 30,
|
|
52
|
+
nonceCache: config.nonceCache ?? new MemoryNonceCacheProvider(),
|
|
53
|
+
...(config.absoluteSessionLifetime !== undefined && {
|
|
54
|
+
absoluteSessionLifetime: config.absoluteSessionLifetime,
|
|
55
|
+
}),
|
|
56
|
+
...(config.serverDid !== undefined && { serverDid: config.serverDid }),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (this.config.nonceCache instanceof MemoryNonceCacheProvider) {
|
|
60
|
+
logger.warn(
|
|
61
|
+
'[SessionManager] Using MemoryNonceCacheProvider — not suitable for ' +
|
|
62
|
+
'multi-instance deployments. Use Redis, DynamoDB, or Cloudflare KV ' +
|
|
63
|
+
'for production.'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setServerDid(serverDid: string): void {
|
|
69
|
+
this.config.serverDid = serverDid;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate an MCP-I handshake request and create a session.
|
|
74
|
+
*
|
|
75
|
+
* Performs the following checks:
|
|
76
|
+
* - Timestamp within acceptable skew window
|
|
77
|
+
* - Audience matches server DID (if configured)
|
|
78
|
+
* - Nonce not previously used (replay protection)
|
|
79
|
+
*
|
|
80
|
+
* @param request - The handshake request containing nonce, audience, timestamp, and optional agentDid
|
|
81
|
+
* @returns Result object with success flag, session on success, or error details on failure
|
|
82
|
+
*/
|
|
83
|
+
async validateHandshake(request: HandshakeRequest): Promise<HandshakeResult> {
|
|
84
|
+
try {
|
|
85
|
+
const now = Math.floor(Date.now() / 1000);
|
|
86
|
+
const timeDiff = Math.abs(now - request.timestamp);
|
|
87
|
+
|
|
88
|
+
if (timeDiff > this.config.timestampSkewSeconds) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
error: {
|
|
92
|
+
code: 'XMCP_I_EHANDSHAKE',
|
|
93
|
+
message: `Timestamp outside acceptable range (±${this.config.timestampSkewSeconds}s)`,
|
|
94
|
+
remediation: `Check NTP sync on client and server. Current server time: ${now}, received: ${request.timestamp}, diff: ${timeDiff}s. Adjust timestampSkewSeconds if needed.`,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Validate audience matches this server's DID (SPEC.md §4 MUST)
|
|
100
|
+
if (this.config.serverDid && request.audience !== this.config.serverDid) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
error: {
|
|
104
|
+
code: 'MCPI_AUDIENCE_MISMATCH',
|
|
105
|
+
message: `Audience mismatch: expected ${this.config.serverDid}, got ${request.audience}`,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const nonceExists = await this.config.nonceCache.has(
|
|
111
|
+
request.nonce,
|
|
112
|
+
request.agentDid
|
|
113
|
+
);
|
|
114
|
+
if (nonceExists) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: {
|
|
118
|
+
code: 'XMCP_I_EHANDSHAKE',
|
|
119
|
+
message: 'Nonce already used (replay attack prevention)',
|
|
120
|
+
remediation: 'Generate a new unique nonce for each request',
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const nonceTtlSeconds = this.config.sessionTtlMinutes * 60 + 60;
|
|
126
|
+
await this.config.nonceCache.add(
|
|
127
|
+
request.nonce,
|
|
128
|
+
nonceTtlSeconds,
|
|
129
|
+
request.agentDid
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const sessionId = await this.generateSessionId();
|
|
133
|
+
const clientInfo = await this.buildClientInfo(request);
|
|
134
|
+
|
|
135
|
+
const session: SessionContext = {
|
|
136
|
+
sessionId,
|
|
137
|
+
audience: request.audience,
|
|
138
|
+
nonce: request.nonce,
|
|
139
|
+
timestamp: request.timestamp,
|
|
140
|
+
createdAt: now,
|
|
141
|
+
lastActivity: now,
|
|
142
|
+
ttlMinutes: this.config.sessionTtlMinutes,
|
|
143
|
+
identityState: 'anonymous',
|
|
144
|
+
agentDid: request.agentDid,
|
|
145
|
+
...(this.config.serverDid && { serverDid: this.config.serverDid }),
|
|
146
|
+
...(clientInfo && { clientInfo }),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
this.sessions.set(sessionId, session);
|
|
150
|
+
|
|
151
|
+
return { success: true, session };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: {
|
|
156
|
+
code: 'XMCP_I_EHANDSHAKE',
|
|
157
|
+
message: `Handshake validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Retrieve a session by ID, checking for expiration.
|
|
165
|
+
*
|
|
166
|
+
* Updates lastActivity timestamp on successful retrieval (sliding window expiry).
|
|
167
|
+
* Returns null if session doesn't exist, has exceeded idle TTL, or has exceeded
|
|
168
|
+
* absolute lifetime (if configured).
|
|
169
|
+
*
|
|
170
|
+
* @param sessionId - The session ID (e.g., "mcpi_...")
|
|
171
|
+
* @returns Session context if valid, null if expired or not found
|
|
172
|
+
*/
|
|
173
|
+
async getSession(sessionId: string): Promise<SessionContext | null> {
|
|
174
|
+
const session = this.sessions.get(sessionId);
|
|
175
|
+
if (!session) return null;
|
|
176
|
+
|
|
177
|
+
const now = Math.floor(Date.now() / 1000);
|
|
178
|
+
const idleTimeSeconds = now - session.lastActivity;
|
|
179
|
+
const maxIdleSeconds = session.ttlMinutes * 60;
|
|
180
|
+
|
|
181
|
+
if (idleTimeSeconds > maxIdleSeconds) {
|
|
182
|
+
this.sessions.delete(sessionId);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (this.config.absoluteSessionLifetime !== undefined) {
|
|
187
|
+
const sessionAgeSeconds = now - session.createdAt;
|
|
188
|
+
const maxAgeSeconds = this.config.absoluteSessionLifetime * 60;
|
|
189
|
+
if (sessionAgeSeconds > maxAgeSeconds) {
|
|
190
|
+
this.sessions.delete(sessionId);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
session.lastActivity = now;
|
|
196
|
+
this.sessions.set(sessionId, session);
|
|
197
|
+
return session;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async generateSessionId(): Promise<string> {
|
|
201
|
+
const bytes = await this.cryptoProvider.randomBytes(16);
|
|
202
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
|
203
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
204
|
+
const hex = Array.from(bytes)
|
|
205
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
206
|
+
.join('');
|
|
207
|
+
const uuid = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
208
|
+
return `mcpi_${uuid}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private async generateClientId(): Promise<string> {
|
|
212
|
+
const bytes = await this.cryptoProvider.randomBytes(6);
|
|
213
|
+
const hex = Array.from(bytes)
|
|
214
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
215
|
+
.join('');
|
|
216
|
+
return `client_${hex}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private normalizeClientInfoString(value: unknown): string | undefined {
|
|
220
|
+
if (typeof value !== 'string') return undefined;
|
|
221
|
+
const trimmed = value.trim();
|
|
222
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async buildClientInfo(
|
|
226
|
+
request: HandshakeRequest
|
|
227
|
+
): Promise<SessionContext['clientInfo'] | undefined> {
|
|
228
|
+
const hasMetadata =
|
|
229
|
+
!!request.clientInfo ||
|
|
230
|
+
typeof request.clientProtocolVersion === 'string' ||
|
|
231
|
+
request.clientCapabilities !== undefined;
|
|
232
|
+
|
|
233
|
+
if (!hasMetadata) return undefined;
|
|
234
|
+
|
|
235
|
+
const source = request.clientInfo;
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
name: this.normalizeClientInfoString(source?.name) ?? 'unknown',
|
|
239
|
+
title: this.normalizeClientInfoString(source?.title),
|
|
240
|
+
version: this.normalizeClientInfoString(source?.version),
|
|
241
|
+
platform: this.normalizeClientInfoString(source?.platform),
|
|
242
|
+
vendor: this.normalizeClientInfoString(source?.vendor),
|
|
243
|
+
persistentId: this.normalizeClientInfoString(source?.persistentId),
|
|
244
|
+
clientId:
|
|
245
|
+
this.normalizeClientInfoString(source?.clientId) ??
|
|
246
|
+
(await this.generateClientId()),
|
|
247
|
+
protocolVersion: this.normalizeClientInfoString(request.clientProtocolVersion),
|
|
248
|
+
capabilities: request.clientCapabilities,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static generateNonce(): string {
|
|
253
|
+
const buffer = new Uint8Array(16);
|
|
254
|
+
globalThis.crypto.getRandomValues(buffer);
|
|
255
|
+
let binaryStr = '';
|
|
256
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
257
|
+
binaryStr += String.fromCharCode(buffer[i]!);
|
|
258
|
+
}
|
|
259
|
+
return btoa(binaryStr)
|
|
260
|
+
.replace(/\+/g, '-')
|
|
261
|
+
.replace(/\//g, '_')
|
|
262
|
+
.replace(/=/g, '');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async cleanup(): Promise<void> {
|
|
266
|
+
const now = Math.floor(Date.now() / 1000);
|
|
267
|
+
|
|
268
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
269
|
+
const idleTimeSeconds = now - session.lastActivity;
|
|
270
|
+
const maxIdleSeconds = session.ttlMinutes * 60;
|
|
271
|
+
let expired = idleTimeSeconds > maxIdleSeconds;
|
|
272
|
+
|
|
273
|
+
if (!expired && this.config.absoluteSessionLifetime !== undefined) {
|
|
274
|
+
const sessionAgeSeconds = now - session.createdAt;
|
|
275
|
+
const maxAgeSeconds = this.config.absoluteSessionLifetime * 60;
|
|
276
|
+
expired = sessionAgeSeconds > maxAgeSeconds;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (expired) {
|
|
280
|
+
this.sessions.delete(sessionId);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await this.config.nonceCache.cleanup();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getStats(): {
|
|
288
|
+
activeSessions: number;
|
|
289
|
+
config: {
|
|
290
|
+
timestampSkewSeconds: number;
|
|
291
|
+
sessionTtlMinutes: number;
|
|
292
|
+
absoluteSessionLifetime?: number;
|
|
293
|
+
cacheType: string;
|
|
294
|
+
};
|
|
295
|
+
} {
|
|
296
|
+
return {
|
|
297
|
+
activeSessions: this.sessions.size,
|
|
298
|
+
config: {
|
|
299
|
+
timestampSkewSeconds: this.config.timestampSkewSeconds,
|
|
300
|
+
sessionTtlMinutes: this.config.sessionTtlMinutes,
|
|
301
|
+
absoluteSessionLifetime: this.config.absoluteSessionLifetime,
|
|
302
|
+
cacheType: this.config.nonceCache.constructor.name,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
clearSessions(): void {
|
|
308
|
+
this.sessions.clear();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function createHandshakeRequest(audience: string): HandshakeRequest {
|
|
313
|
+
return {
|
|
314
|
+
nonce: SessionManager.generateNonce(),
|
|
315
|
+
audience,
|
|
316
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function validateHandshakeFormat(request: unknown): request is HandshakeRequest {
|
|
321
|
+
return (
|
|
322
|
+
typeof request === 'object' &&
|
|
323
|
+
request !== null &&
|
|
324
|
+
typeof (request as Record<string, unknown>)['nonce'] === 'string' &&
|
|
325
|
+
((request as Record<string, unknown>)['nonce'] as string).length > 0 &&
|
|
326
|
+
typeof (request as Record<string, unknown>)['audience'] === 'string' &&
|
|
327
|
+
((request as Record<string, unknown>)['audience'] as string).length > 0 &&
|
|
328
|
+
typeof (request as Record<string, unknown>)['timestamp'] === 'number' &&
|
|
329
|
+
((request as Record<string, unknown>)['timestamp'] as number) > 0 &&
|
|
330
|
+
Number.isInteger((request as Record<string, unknown>)['timestamp'])
|
|
331
|
+
);
|
|
332
|
+
}
|