@kya-os/mcp-i 1.2.2 → 1.2.3
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/dist/compiler/get-webpack-config/get-externals.js +2 -2
- package/dist/compiler/get-webpack-config/plugins.js +1 -13
- package/dist/runtime/session.js +4 -2
- package/dist/storage/encryption.d.ts +61 -0
- package/dist/storage/encryption.js +151 -0
- package/dist/storage/index.d.ts +11 -0
- package/dist/storage/index.js +26 -0
- package/package.json +2 -2
- package/dist/cache/__tests__/cloudflare-kv-nonce-cache.test.d.ts +0 -4
- package/dist/cache/__tests__/cloudflare-kv-nonce-cache.test.js +0 -176
- package/dist/cache/__tests__/concurrency.test.d.ts +0 -5
- package/dist/cache/__tests__/concurrency.test.js +0 -300
- package/dist/cache/__tests__/dynamodb-nonce-cache.test.d.ts +0 -4
- package/dist/cache/__tests__/dynamodb-nonce-cache.test.js +0 -176
- package/dist/cache/__tests__/memory-nonce-cache.test.d.ts +0 -4
- package/dist/cache/__tests__/memory-nonce-cache.test.js +0 -132
- package/dist/cache/__tests__/nonce-cache-factory-simple.test.d.ts +0 -4
- package/dist/cache/__tests__/nonce-cache-factory-simple.test.js +0 -133
- package/dist/cache/__tests__/nonce-cache-factory.test.d.ts +0 -4
- package/dist/cache/__tests__/nonce-cache-factory.test.js +0 -252
- package/dist/cache/__tests__/redis-nonce-cache.test.d.ts +0 -4
- package/dist/cache/__tests__/redis-nonce-cache.test.js +0 -95
- package/dist/runtime/__tests__/audit.test.d.ts +0 -4
- package/dist/runtime/__tests__/audit.test.js +0 -328
- package/dist/runtime/__tests__/identity.test.d.ts +0 -4
- package/dist/runtime/__tests__/identity.test.js +0 -164
- package/dist/runtime/__tests__/mcpi-runtime.test.d.ts +0 -4
- package/dist/runtime/__tests__/mcpi-runtime.test.js +0 -372
- package/dist/runtime/__tests__/proof.test.d.ts +0 -4
- package/dist/runtime/__tests__/proof.test.js +0 -302
- package/dist/runtime/__tests__/session.test.d.ts +0 -4
- package/dist/runtime/__tests__/session.test.js +0 -254
- package/dist/runtime/__tests__/well-known.test.d.ts +0 -4
- package/dist/runtime/__tests__/well-known.test.js +0 -312
- package/dist/test/__tests__/nonce-cache-integration.test.d.ts +0 -1
- package/dist/test/__tests__/nonce-cache-integration.test.js +0 -116
- package/dist/test/__tests__/nonce-cache.test.d.ts +0 -1
- package/dist/test/__tests__/nonce-cache.test.js +0 -122
- package/dist/test/__tests__/runtime-integration.test.d.ts +0 -4
- package/dist/test/__tests__/runtime-integration.test.js +0 -192
- package/dist/test/__tests__/test-infrastructure.test.d.ts +0 -4
- package/dist/test/__tests__/test-infrastructure.test.js +0 -178
|
@@ -70,8 +70,8 @@ function getExternals() {
|
|
|
70
70
|
}
|
|
71
71
|
let pathRequest = request;
|
|
72
72
|
/**
|
|
73
|
-
* Paths are relative to the .
|
|
74
|
-
* but we are building in .
|
|
73
|
+
* Paths are relative to the .mcpi/nextjs-adapter folder,
|
|
74
|
+
* but we are building in .mcpi/adapter/index.js, so we need to go up 2 levels
|
|
75
75
|
*/
|
|
76
76
|
if (request.startsWith("../")) {
|
|
77
77
|
// Only replace the import if it hasn't been replaced yet
|
|
@@ -12,7 +12,6 @@ const compiler_context_1 = require("../compiler-context");
|
|
|
12
12
|
const getDefaultRuntimeFiles = () => {
|
|
13
13
|
const path = require("path");
|
|
14
14
|
const fs = require("fs");
|
|
15
|
-
console.log("getDefaultRuntimeFiles: Starting from __dirname:", __dirname);
|
|
16
15
|
// Try multiple possible locations for the mcp-i runtime dist directory
|
|
17
16
|
const possiblePaths = [
|
|
18
17
|
// When running from compiled dist
|
|
@@ -27,17 +26,14 @@ const getDefaultRuntimeFiles = () => {
|
|
|
27
26
|
];
|
|
28
27
|
let mcpDistPath = "";
|
|
29
28
|
for (const possiblePath of possiblePaths) {
|
|
30
|
-
console.log("getDefaultRuntimeFiles: Checking path:", possiblePath);
|
|
31
29
|
if (fs.existsSync(possiblePath) &&
|
|
32
30
|
fs.existsSync(path.join(possiblePath, "http.js"))) {
|
|
33
31
|
mcpDistPath = possiblePath;
|
|
34
|
-
console.log("getDefaultRuntimeFiles: Found valid dist path:", mcpDistPath);
|
|
35
32
|
break;
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
const runtimeFiles = {};
|
|
39
36
|
if (mcpDistPath) {
|
|
40
|
-
console.log("getDefaultRuntimeFiles: Loading runtime files from:", mcpDistPath);
|
|
41
37
|
// Add known runtime files
|
|
42
38
|
const knownFiles = [
|
|
43
39
|
"http.js",
|
|
@@ -48,17 +44,11 @@ const getDefaultRuntimeFiles = () => {
|
|
|
48
44
|
];
|
|
49
45
|
for (const file of knownFiles) {
|
|
50
46
|
const filePath = path.join(mcpDistPath, file);
|
|
51
|
-
console.log("getDefaultRuntimeFiles: Checking for file:", filePath);
|
|
52
47
|
if (fs.existsSync(filePath)) {
|
|
53
|
-
console.log("getDefaultRuntimeFiles: Found file:", file);
|
|
54
48
|
runtimeFiles[file] = fs.readFileSync(filePath, "utf8");
|
|
55
49
|
}
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
|
-
else {
|
|
59
|
-
console.log("getDefaultRuntimeFiles: No valid dist path found");
|
|
60
|
-
}
|
|
61
|
-
console.log("getDefaultRuntimeFiles: Final runtime files:", Object.keys(runtimeFiles));
|
|
62
52
|
return runtimeFiles;
|
|
63
53
|
};
|
|
64
54
|
exports.runtimeFiles = (typeof RUNTIME_FILES !== "undefined"
|
|
@@ -71,10 +61,8 @@ class InjectRuntimePlugin {
|
|
|
71
61
|
if (hasRun)
|
|
72
62
|
return;
|
|
73
63
|
hasRun = true;
|
|
74
|
-
console.log("InjectRuntimePlugin: Found runtime files:", Object.keys(exports.runtimeFiles));
|
|
75
64
|
for (const [fileName, fileContent] of Object.entries(exports.runtimeFiles)) {
|
|
76
65
|
const targetPath = path_1.default.join(constants_1.runtimeFolderPath, fileName);
|
|
77
|
-
console.log(`InjectRuntimePlugin: Writing ${fileName} to ${targetPath}`);
|
|
78
66
|
fs_extra_1.default.writeFileSync(targetPath, fileContent);
|
|
79
67
|
}
|
|
80
68
|
});
|
|
@@ -114,7 +102,7 @@ class CreateTypeDefinitionPlugin {
|
|
|
114
102
|
return;
|
|
115
103
|
hasRun = true;
|
|
116
104
|
const xmcpConfig = (0, compiler_context_1.getXmcpConfig)();
|
|
117
|
-
// Manually type the .
|
|
105
|
+
// Manually type the .mcpi/adapter/index.js file using a .mcpi/adapter/index.d.ts file
|
|
118
106
|
// TO DO add withAuth to the type definition & AuthConfig
|
|
119
107
|
if (xmcpConfig.experimental?.adapter) {
|
|
120
108
|
let typeDefinitionContent = "";
|
package/dist/runtime/session.js
CHANGED
|
@@ -25,8 +25,10 @@ class SessionManager {
|
|
|
25
25
|
nonceCache: new memory_nonce_cache_1.MemoryNonceCache(),
|
|
26
26
|
...config,
|
|
27
27
|
};
|
|
28
|
-
// Warn about multi-instance deployments with memory cache
|
|
29
|
-
if (this.config.nonceCache instanceof memory_nonce_cache_1.MemoryNonceCache
|
|
28
|
+
// Warn about multi-instance deployments with memory cache (only in production or if explicitly enabled)
|
|
29
|
+
if (this.config.nonceCache instanceof memory_nonce_cache_1.MemoryNonceCache &&
|
|
30
|
+
(process.env.NODE_ENV === "production" ||
|
|
31
|
+
process.env.MCPI_WARN_MEMORY_CACHE === "true")) {
|
|
30
32
|
console.warn("Warning: Using MemoryNonceCache - not suitable for multi-instance deployments. " +
|
|
31
33
|
"Consider using Redis, DynamoDB, or Cloudflare KV for production.");
|
|
32
34
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption utilities for ktaEncrypted storage mode
|
|
3
|
+
*/
|
|
4
|
+
export interface EncryptionResult {
|
|
5
|
+
ciphertext: string;
|
|
6
|
+
nonce: string;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DecryptionParams {
|
|
10
|
+
ciphertext: string;
|
|
11
|
+
nonce: string;
|
|
12
|
+
publicKey: string;
|
|
13
|
+
privateKey: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Audience-key encryption for ktaEncrypted mode
|
|
17
|
+
* Uses X25519 key exchange + ChaCha20-Poly1305 AEAD
|
|
18
|
+
*/
|
|
19
|
+
export declare class AudienceKeyEncryption {
|
|
20
|
+
/**
|
|
21
|
+
* Generate a new key pair for encryption
|
|
22
|
+
*/
|
|
23
|
+
static generateKeyPair(): Promise<CryptoKeyPair>;
|
|
24
|
+
/**
|
|
25
|
+
* Encrypt data for a specific audience public key
|
|
26
|
+
*/
|
|
27
|
+
static encrypt(data: string, audiencePublicKey: string): Promise<EncryptionResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Decrypt data using private key
|
|
30
|
+
*/
|
|
31
|
+
static decrypt(params: DecryptionParams): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Import public key from base64 string
|
|
34
|
+
*/
|
|
35
|
+
private static importPublicKey;
|
|
36
|
+
/**
|
|
37
|
+
* Import private key from base64 string
|
|
38
|
+
*/
|
|
39
|
+
private static importPrivateKey;
|
|
40
|
+
/**
|
|
41
|
+
* Export public key to base64 string
|
|
42
|
+
*/
|
|
43
|
+
private static exportPublicKey;
|
|
44
|
+
/**
|
|
45
|
+
* Export private key to base64 string
|
|
46
|
+
*/
|
|
47
|
+
static exportPrivateKey(privateKey: CryptoKey): Promise<string>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Simple symmetric encryption for testing
|
|
51
|
+
*/
|
|
52
|
+
export declare class SimpleEncryption {
|
|
53
|
+
/**
|
|
54
|
+
* Encrypt data with a password (for testing only)
|
|
55
|
+
*/
|
|
56
|
+
static encrypt(data: string, password: string): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Decrypt data with a password (for testing only)
|
|
59
|
+
*/
|
|
60
|
+
static decrypt(encryptedData: string, password: string): Promise<string>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SimpleEncryption = exports.AudienceKeyEncryption = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/**
|
|
6
|
+
* Audience-key encryption for ktaEncrypted mode
|
|
7
|
+
* Uses X25519 key exchange + ChaCha20-Poly1305 AEAD
|
|
8
|
+
*/
|
|
9
|
+
class AudienceKeyEncryption {
|
|
10
|
+
/**
|
|
11
|
+
* Generate a new key pair for encryption
|
|
12
|
+
*/
|
|
13
|
+
static async generateKeyPair() {
|
|
14
|
+
const keyPair = await crypto_1.webcrypto.subtle.generateKey({
|
|
15
|
+
name: "X25519",
|
|
16
|
+
}, true, // extractable
|
|
17
|
+
["deriveKey"]);
|
|
18
|
+
// Type assertion since we know X25519 returns a CryptoKeyPair
|
|
19
|
+
return keyPair;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Encrypt data for a specific audience public key
|
|
23
|
+
*/
|
|
24
|
+
static async encrypt(data, audiencePublicKey) {
|
|
25
|
+
// Generate ephemeral key pair
|
|
26
|
+
const ephemeralKeyPair = await this.generateKeyPair();
|
|
27
|
+
// Import audience public key
|
|
28
|
+
const audiencePubKey = await this.importPublicKey(audiencePublicKey);
|
|
29
|
+
// Derive shared secret
|
|
30
|
+
const sharedSecret = await crypto_1.webcrypto.subtle.deriveKey({
|
|
31
|
+
name: "X25519",
|
|
32
|
+
public: audiencePubKey,
|
|
33
|
+
}, ephemeralKeyPair.privateKey, {
|
|
34
|
+
name: "ChaCha20-Poly1305",
|
|
35
|
+
length: 256,
|
|
36
|
+
}, false, // not extractable
|
|
37
|
+
["encrypt"]);
|
|
38
|
+
// Generate random nonce
|
|
39
|
+
const nonce = crypto_1.webcrypto.getRandomValues(new Uint8Array(12));
|
|
40
|
+
// Encrypt the data
|
|
41
|
+
const encoder = new TextEncoder();
|
|
42
|
+
const dataBytes = encoder.encode(data);
|
|
43
|
+
const ciphertext = await crypto_1.webcrypto.subtle.encrypt({
|
|
44
|
+
name: "ChaCha20-Poly1305",
|
|
45
|
+
iv: nonce,
|
|
46
|
+
}, sharedSecret, dataBytes);
|
|
47
|
+
// Export ephemeral public key
|
|
48
|
+
const ephemeralPublicKey = await this.exportPublicKey(ephemeralKeyPair.publicKey);
|
|
49
|
+
return {
|
|
50
|
+
ciphertext: Buffer.from(ciphertext).toString("base64"),
|
|
51
|
+
nonce: Buffer.from(nonce).toString("base64"),
|
|
52
|
+
publicKey: ephemeralPublicKey,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decrypt data using private key
|
|
57
|
+
*/
|
|
58
|
+
static async decrypt(params) {
|
|
59
|
+
// Import keys
|
|
60
|
+
const ephemeralPublicKey = await this.importPublicKey(params.publicKey);
|
|
61
|
+
const privateKey = await this.importPrivateKey(params.privateKey);
|
|
62
|
+
// Derive shared secret
|
|
63
|
+
const sharedSecret = await crypto_1.webcrypto.subtle.deriveKey({
|
|
64
|
+
name: "X25519",
|
|
65
|
+
public: ephemeralPublicKey,
|
|
66
|
+
}, privateKey, {
|
|
67
|
+
name: "ChaCha20-Poly1305",
|
|
68
|
+
length: 256,
|
|
69
|
+
}, false, // not extractable
|
|
70
|
+
["decrypt"]);
|
|
71
|
+
// Decrypt the data
|
|
72
|
+
const ciphertext = Buffer.from(params.ciphertext, "base64");
|
|
73
|
+
const nonce = Buffer.from(params.nonce, "base64");
|
|
74
|
+
const decrypted = await crypto_1.webcrypto.subtle.decrypt({
|
|
75
|
+
name: "ChaCha20-Poly1305",
|
|
76
|
+
iv: nonce,
|
|
77
|
+
}, sharedSecret, ciphertext);
|
|
78
|
+
const decoder = new TextDecoder();
|
|
79
|
+
return decoder.decode(decrypted);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Import public key from base64 string
|
|
83
|
+
*/
|
|
84
|
+
static async importPublicKey(publicKey) {
|
|
85
|
+
const keyBytes = Buffer.from(publicKey, "base64");
|
|
86
|
+
return await crypto_1.webcrypto.subtle.importKey("raw", keyBytes, {
|
|
87
|
+
name: "X25519",
|
|
88
|
+
}, false, []);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Import private key from base64 string
|
|
92
|
+
*/
|
|
93
|
+
static async importPrivateKey(privateKey) {
|
|
94
|
+
const keyBytes = Buffer.from(privateKey, "base64");
|
|
95
|
+
return await crypto_1.webcrypto.subtle.importKey("raw", keyBytes, {
|
|
96
|
+
name: "X25519",
|
|
97
|
+
}, false, ["deriveKey"]);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Export public key to base64 string
|
|
101
|
+
*/
|
|
102
|
+
static async exportPublicKey(publicKey) {
|
|
103
|
+
const keyBytes = await crypto_1.webcrypto.subtle.exportKey("raw", publicKey);
|
|
104
|
+
return Buffer.from(keyBytes).toString("base64");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Export private key to base64 string
|
|
108
|
+
*/
|
|
109
|
+
static async exportPrivateKey(privateKey) {
|
|
110
|
+
const keyBytes = await crypto_1.webcrypto.subtle.exportKey("raw", privateKey);
|
|
111
|
+
return Buffer.from(keyBytes).toString("base64");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.AudienceKeyEncryption = AudienceKeyEncryption;
|
|
115
|
+
/**
|
|
116
|
+
* Simple symmetric encryption for testing
|
|
117
|
+
*/
|
|
118
|
+
class SimpleEncryption {
|
|
119
|
+
/**
|
|
120
|
+
* Encrypt data with a password (for testing only)
|
|
121
|
+
*/
|
|
122
|
+
static async encrypt(data, password) {
|
|
123
|
+
// This is a simple implementation for testing
|
|
124
|
+
// In production, use proper key derivation and authenticated encryption
|
|
125
|
+
const encoder = new TextEncoder();
|
|
126
|
+
const dataBytes = encoder.encode(data);
|
|
127
|
+
const passwordBytes = encoder.encode(password);
|
|
128
|
+
// Simple XOR encryption (NOT secure, for testing only)
|
|
129
|
+
const encrypted = new Uint8Array(dataBytes.length);
|
|
130
|
+
for (let i = 0; i < dataBytes.length; i++) {
|
|
131
|
+
encrypted[i] = dataBytes[i] ^ passwordBytes[i % passwordBytes.length];
|
|
132
|
+
}
|
|
133
|
+
return Buffer.from(encrypted).toString("base64");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Decrypt data with a password (for testing only)
|
|
137
|
+
*/
|
|
138
|
+
static async decrypt(encryptedData, password) {
|
|
139
|
+
const encrypted = Buffer.from(encryptedData, "base64");
|
|
140
|
+
const encoder = new TextEncoder();
|
|
141
|
+
const passwordBytes = encoder.encode(password);
|
|
142
|
+
// Simple XOR decryption (NOT secure, for testing only)
|
|
143
|
+
const decrypted = new Uint8Array(encrypted.length);
|
|
144
|
+
for (let i = 0; i < encrypted.length; i++) {
|
|
145
|
+
decrypted[i] = encrypted[i] ^ passwordBytes[i % passwordBytes.length];
|
|
146
|
+
}
|
|
147
|
+
const decoder = new TextDecoder();
|
|
148
|
+
return decoder.decode(decrypted);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
exports.SimpleEncryption = SimpleEncryption;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage and receipts module for XMCP-I
|
|
3
|
+
*
|
|
4
|
+
* This module provides storage mode configuration, receipt handling,
|
|
5
|
+
* delegation management, and encryption utilities for verifiable credentials.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./config";
|
|
8
|
+
export * from "./delegation";
|
|
9
|
+
export * from "./encryption";
|
|
10
|
+
export * from "./merkle-verifier";
|
|
11
|
+
export type { StorageMode, StorageConfig, Receipt, Delegation, DelegationRequest, DelegationResponse, } from "@kya-os/contracts/registry";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Storage and receipts module for XMCP-I
|
|
4
|
+
*
|
|
5
|
+
* This module provides storage mode configuration, receipt handling,
|
|
6
|
+
* delegation management, and encryption utilities for verifiable credentials.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
20
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
__exportStar(require("./config"), exports);
|
|
24
|
+
__exportStar(require("./delegation"), exports);
|
|
25
|
+
__exportStar(require("./encryption"), exports);
|
|
26
|
+
__exportStar(require("./merkle-verifier"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kya-os/mcp-i",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "The TypeScript MCP framework with identity features built-in",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"model-context-protocol"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@kya-os/contracts": "
|
|
49
|
+
"@kya-os/contracts": "workspace:*",
|
|
50
50
|
"@modelcontextprotocol/inspector": "^0.16.6",
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.11.4",
|
|
52
52
|
"@swc/core": "^1.11.24",
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Tests for Cloudflare KV Nonce Cache
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const vitest_1 = require("vitest");
|
|
7
|
-
const cloudflare_kv_nonce_cache_js_1 = require("../cloudflare-kv-nonce-cache.js");
|
|
8
|
-
// Mock Cloudflare KV namespace
|
|
9
|
-
const mockKV = {
|
|
10
|
-
get: vitest_1.vi.fn(),
|
|
11
|
-
getWithMetadata: vitest_1.vi.fn(),
|
|
12
|
-
put: vitest_1.vi.fn(),
|
|
13
|
-
delete: vitest_1.vi.fn(),
|
|
14
|
-
};
|
|
15
|
-
(0, vitest_1.describe)("CloudflareKVNonceCache", () => {
|
|
16
|
-
let cache;
|
|
17
|
-
(0, vitest_1.beforeEach)(() => {
|
|
18
|
-
vitest_1.vi.clearAllMocks();
|
|
19
|
-
cache = new cloudflare_kv_nonce_cache_js_1.CloudflareKVNonceCache(mockKV, "test:");
|
|
20
|
-
});
|
|
21
|
-
(0, vitest_1.describe)("Basic Operations", () => {
|
|
22
|
-
(0, vitest_1.it)("should add and check nonce existence", async () => {
|
|
23
|
-
const nonce = "test-nonce-123";
|
|
24
|
-
// Mock KV responses
|
|
25
|
-
mockKV.get.mockResolvedValue(null); // doesn't exist initially
|
|
26
|
-
mockKV.put.mockResolvedValue(undefined); // successful put
|
|
27
|
-
// Initially should not exist
|
|
28
|
-
(0, vitest_1.expect)(await cache.has(nonce)).toBe(false);
|
|
29
|
-
(0, vitest_1.expect)(mockKV.get).toHaveBeenCalledWith("test:test-nonce-123");
|
|
30
|
-
// Mock getWithMetadata for add operation
|
|
31
|
-
mockKV.getWithMetadata.mockResolvedValue({ value: null, metadata: null });
|
|
32
|
-
// Add nonce
|
|
33
|
-
await cache.add(nonce, 60);
|
|
34
|
-
(0, vitest_1.expect)(mockKV.put).toHaveBeenCalledWith("test:test-nonce-123", vitest_1.expect.stringContaining('"nonce":"test-nonce-123"'), { expirationTtl: 60 });
|
|
35
|
-
// Mock that it now exists and is valid
|
|
36
|
-
const futureTime = Date.now() + 50000;
|
|
37
|
-
mockKV.get.mockResolvedValue(JSON.stringify({
|
|
38
|
-
nonce,
|
|
39
|
-
expiresAt: futureTime,
|
|
40
|
-
createdAt: Date.now(),
|
|
41
|
-
}));
|
|
42
|
-
(0, vitest_1.expect)(await cache.has(nonce)).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
(0, vitest_1.it)("should handle expired nonces correctly", async () => {
|
|
45
|
-
const nonce = "expired-nonce";
|
|
46
|
-
// Mock expired nonce data
|
|
47
|
-
const pastTime = Date.now() - 10000;
|
|
48
|
-
mockKV.get.mockResolvedValue(JSON.stringify({
|
|
49
|
-
nonce,
|
|
50
|
-
expiresAt: pastTime,
|
|
51
|
-
createdAt: Date.now() - 20000,
|
|
52
|
-
}));
|
|
53
|
-
mockKV.delete.mockResolvedValue(undefined);
|
|
54
|
-
// Should return false and clean up expired entry
|
|
55
|
-
(0, vitest_1.expect)(await cache.has(nonce)).toBe(false);
|
|
56
|
-
(0, vitest_1.expect)(mockKV.delete).toHaveBeenCalledWith("test:expired-nonce");
|
|
57
|
-
});
|
|
58
|
-
(0, vitest_1.it)("should handle corrupted data gracefully", async () => {
|
|
59
|
-
const nonce = "corrupted-nonce";
|
|
60
|
-
// Mock corrupted JSON data
|
|
61
|
-
mockKV.get.mockResolvedValue("invalid-json");
|
|
62
|
-
mockKV.delete.mockResolvedValue(undefined);
|
|
63
|
-
// Should return false and clean up corrupted entry
|
|
64
|
-
(0, vitest_1.expect)(await cache.has(nonce)).toBe(false);
|
|
65
|
-
(0, vitest_1.expect)(mockKV.delete).toHaveBeenCalledWith("test:corrupted-nonce");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
(0, vitest_1.describe)("Atomic Operations with getWithMetadata", () => {
|
|
69
|
-
(0, vitest_1.it)("should use getWithMetadata for better atomicity when available", async () => {
|
|
70
|
-
const nonce = "atomic-test-nonce";
|
|
71
|
-
// Mock getWithMetadata returning no existing value
|
|
72
|
-
mockKV.getWithMetadata.mockResolvedValue({ value: null, metadata: null });
|
|
73
|
-
mockKV.put.mockResolvedValue(undefined);
|
|
74
|
-
await cache.add(nonce, 60);
|
|
75
|
-
(0, vitest_1.expect)(mockKV.getWithMetadata).toHaveBeenCalledWith("test:atomic-test-nonce");
|
|
76
|
-
(0, vitest_1.expect)(mockKV.put).toHaveBeenCalled();
|
|
77
|
-
});
|
|
78
|
-
(0, vitest_1.it)("should prevent duplicate addition with getWithMetadata", async () => {
|
|
79
|
-
const nonce = "duplicate-nonce";
|
|
80
|
-
// Mock existing valid nonce
|
|
81
|
-
const futureTime = Date.now() + 50000;
|
|
82
|
-
mockKV.getWithMetadata.mockResolvedValue({
|
|
83
|
-
value: JSON.stringify({
|
|
84
|
-
nonce,
|
|
85
|
-
expiresAt: futureTime,
|
|
86
|
-
createdAt: Date.now(),
|
|
87
|
-
}),
|
|
88
|
-
metadata: null,
|
|
89
|
-
});
|
|
90
|
-
await (0, vitest_1.expect)(cache.add(nonce, 60)).rejects.toThrow("Nonce duplicate-nonce already exists - potential replay attack");
|
|
91
|
-
});
|
|
92
|
-
(0, vitest_1.it)("should allow overwrite of expired nonce with getWithMetadata", async () => {
|
|
93
|
-
const nonce = "expired-overwrite-nonce";
|
|
94
|
-
// Mock existing expired nonce
|
|
95
|
-
const pastTime = Date.now() - 10000;
|
|
96
|
-
mockKV.getWithMetadata.mockResolvedValue({
|
|
97
|
-
value: JSON.stringify({
|
|
98
|
-
nonce,
|
|
99
|
-
expiresAt: pastTime,
|
|
100
|
-
createdAt: Date.now() - 20000,
|
|
101
|
-
}),
|
|
102
|
-
metadata: null,
|
|
103
|
-
});
|
|
104
|
-
mockKV.put.mockResolvedValue(undefined);
|
|
105
|
-
// Should succeed in overwriting expired nonce
|
|
106
|
-
await (0, vitest_1.expect)(cache.add(nonce, 60)).resolves.not.toThrow();
|
|
107
|
-
(0, vitest_1.expect)(mockKV.put).toHaveBeenCalled();
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
(0, vitest_1.describe)("Fallback to Basic Operations", () => {
|
|
111
|
-
(0, vitest_1.it)("should fall back to basic operations when getWithMetadata fails", async () => {
|
|
112
|
-
const nonce = "fallback-nonce";
|
|
113
|
-
// Mock getWithMetadata throwing error
|
|
114
|
-
const getWithMetadataError = new Error("getWithMetadata is not available");
|
|
115
|
-
mockKV.getWithMetadata.mockRejectedValue(getWithMetadataError);
|
|
116
|
-
// Mock basic operations
|
|
117
|
-
mockKV.get.mockResolvedValue(null);
|
|
118
|
-
mockKV.put.mockResolvedValue(undefined);
|
|
119
|
-
await cache.add(nonce, 60);
|
|
120
|
-
// Should have fallen back to basic has() check
|
|
121
|
-
(0, vitest_1.expect)(mockKV.get).toHaveBeenCalledWith("test:fallback-nonce");
|
|
122
|
-
(0, vitest_1.expect)(mockKV.put).toHaveBeenCalled();
|
|
123
|
-
});
|
|
124
|
-
(0, vitest_1.it)("should prevent duplicate addition in fallback mode", async () => {
|
|
125
|
-
const nonce = "fallback-duplicate-nonce";
|
|
126
|
-
// Mock getWithMetadata not available
|
|
127
|
-
const getWithMetadataError = new Error("getWithMetadata is not available");
|
|
128
|
-
mockKV.getWithMetadata.mockRejectedValue(getWithMetadataError);
|
|
129
|
-
// Mock existing nonce in basic mode
|
|
130
|
-
const futureTime = Date.now() + 50000;
|
|
131
|
-
mockKV.get.mockResolvedValue(JSON.stringify({
|
|
132
|
-
nonce,
|
|
133
|
-
expiresAt: futureTime,
|
|
134
|
-
createdAt: Date.now(),
|
|
135
|
-
}));
|
|
136
|
-
await (0, vitest_1.expect)(cache.add(nonce, 60)).rejects.toThrow("Nonce fallback-duplicate-nonce already exists - potential replay attack");
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
(0, vitest_1.describe)("Error Handling", () => {
|
|
140
|
-
(0, vitest_1.it)("should propagate unexpected errors", async () => {
|
|
141
|
-
const nonce = "error-nonce";
|
|
142
|
-
const unexpectedError = new Error("Network error");
|
|
143
|
-
mockKV.getWithMetadata.mockRejectedValue(unexpectedError);
|
|
144
|
-
await (0, vitest_1.expect)(cache.add(nonce, 60)).rejects.toThrow("Network error");
|
|
145
|
-
});
|
|
146
|
-
(0, vitest_1.it)("should handle KV operation failures gracefully", async () => {
|
|
147
|
-
const nonce = "kv-error-nonce";
|
|
148
|
-
mockKV.get.mockRejectedValue(new Error("KV service unavailable"));
|
|
149
|
-
// has() should handle KV errors gracefully
|
|
150
|
-
await (0, vitest_1.expect)(cache.has(nonce)).rejects.toThrow("KV service unavailable");
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
(0, vitest_1.describe)("Cleanup", () => {
|
|
154
|
-
(0, vitest_1.it)("should be a no-op since KV handles expiry", async () => {
|
|
155
|
-
// cleanup() should not call any KV methods
|
|
156
|
-
await cache.cleanup();
|
|
157
|
-
(0, vitest_1.expect)(mockKV.get).not.toHaveBeenCalled();
|
|
158
|
-
(0, vitest_1.expect)(mockKV.put).not.toHaveBeenCalled();
|
|
159
|
-
(0, vitest_1.expect)(mockKV.delete).not.toHaveBeenCalled();
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
(0, vitest_1.describe)("Key Prefix", () => {
|
|
163
|
-
(0, vitest_1.it)("should use custom key prefix", async () => {
|
|
164
|
-
const customCache = new cloudflare_kv_nonce_cache_js_1.CloudflareKVNonceCache(mockKV, "custom:");
|
|
165
|
-
mockKV.get.mockResolvedValue(null);
|
|
166
|
-
await customCache.has("test-nonce");
|
|
167
|
-
(0, vitest_1.expect)(mockKV.get).toHaveBeenCalledWith("custom:test-nonce");
|
|
168
|
-
});
|
|
169
|
-
(0, vitest_1.it)("should use default key prefix", async () => {
|
|
170
|
-
const defaultCache = new cloudflare_kv_nonce_cache_js_1.CloudflareKVNonceCache(mockKV);
|
|
171
|
-
mockKV.get.mockResolvedValue(null);
|
|
172
|
-
await defaultCache.has("test-nonce");
|
|
173
|
-
(0, vitest_1.expect)(mockKV.get).toHaveBeenCalledWith("nonce:test-nonce");
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
});
|