@kya-os/mcp-i 0.1.0 → 1.2.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/README.md +406 -71
- package/dist/149.js +1 -0
- package/dist/189.js +1 -0
- package/dist/261.js +1 -0
- package/dist/28.js +1 -0
- package/dist/295.js +1 -0
- package/dist/460.js +1 -0
- package/dist/570.js +1 -0
- package/dist/634.js +1 -0
- package/dist/647.js +1 -0
- package/dist/67.js +1 -0
- package/dist/739.js +1 -0
- package/dist/742.js +1 -0
- package/dist/904.js +1 -0
- package/dist/938.js +1 -0
- package/dist/auth/api-key.d.ts +16 -0
- package/dist/auth/api-key.js +82 -0
- package/dist/auth/jwt.d.ts +43 -0
- package/dist/auth/jwt.js +51 -0
- package/dist/auth/oauth/factory.d.ts +12 -0
- package/dist/auth/oauth/factory.js +36 -0
- package/dist/auth/oauth/index.d.ts +5 -0
- package/dist/auth/oauth/index.js +27 -0
- package/dist/auth/oauth/providers/proxy-provider.d.ts +13 -0
- package/dist/auth/oauth/providers/proxy-provider.js +159 -0
- package/dist/auth/oauth/router.d.ts +4 -0
- package/dist/auth/oauth/router.js +294 -0
- package/dist/auth/oauth/storage/memory-storage.d.ts +12 -0
- package/dist/auth/oauth/storage/memory-storage.js +40 -0
- package/dist/auth/oauth/types.d.ts +112 -0
- package/dist/auth/oauth/types.js +2 -0
- package/dist/cache/__tests__/cloudflare-kv-nonce-cache.test.d.ts +4 -0
- package/dist/cache/__tests__/cloudflare-kv-nonce-cache.test.js +176 -0
- package/dist/cache/__tests__/concurrency.test.d.ts +5 -0
- package/dist/cache/__tests__/concurrency.test.js +300 -0
- package/dist/cache/__tests__/dynamodb-nonce-cache.test.d.ts +4 -0
- package/dist/cache/__tests__/dynamodb-nonce-cache.test.js +176 -0
- package/dist/cache/__tests__/memory-nonce-cache.test.d.ts +4 -0
- package/dist/cache/__tests__/memory-nonce-cache.test.js +132 -0
- package/dist/cache/__tests__/nonce-cache-factory-simple.test.d.ts +4 -0
- package/dist/cache/__tests__/nonce-cache-factory-simple.test.js +133 -0
- package/dist/cache/__tests__/nonce-cache-factory.test.d.ts +4 -0
- package/dist/cache/__tests__/nonce-cache-factory.test.js +252 -0
- package/dist/cache/__tests__/redis-nonce-cache.test.d.ts +4 -0
- package/dist/cache/__tests__/redis-nonce-cache.test.js +95 -0
- package/dist/cache/cloudflare-kv-nonce-cache.d.ts +14 -0
- package/dist/cache/cloudflare-kv-nonce-cache.js +93 -0
- package/dist/cache/dynamodb-nonce-cache.d.ts +15 -0
- package/dist/cache/dynamodb-nonce-cache.js +92 -0
- package/dist/cache/index.d.ts +16 -0
- package/dist/cache/index.js +32 -0
- package/dist/cache/memory-nonce-cache.d.ts +44 -0
- package/dist/cache/memory-nonce-cache.js +105 -0
- package/dist/cache/nonce-cache-factory.d.ts +20 -0
- package/dist/cache/nonce-cache-factory.js +208 -0
- package/dist/cache/redis-nonce-cache.d.ts +14 -0
- package/dist/cache/redis-nonce-cache.js +53 -0
- package/dist/compiler/compiler-context.d.ts +23 -0
- package/dist/compiler/compiler-context.js +24 -0
- package/dist/compiler/config/constants.d.ts +41 -0
- package/dist/compiler/config/constants.js +45 -0
- package/dist/compiler/config/index.d.ts +252 -0
- package/dist/compiler/config/index.js +15 -0
- package/dist/compiler/config/injection.d.ts +26 -0
- package/dist/compiler/config/injection.js +58 -0
- package/dist/compiler/config/schemas/experimental/index.d.ts +91 -0
- package/dist/compiler/config/schemas/experimental/index.js +16 -0
- package/dist/compiler/config/schemas/experimental/oauth.d.ts +74 -0
- package/dist/compiler/config/schemas/experimental/oauth.js +25 -0
- package/dist/compiler/config/schemas/index.d.ts +6 -0
- package/dist/compiler/config/schemas/index.js +17 -0
- package/dist/compiler/config/schemas/paths.d.ts +9 -0
- package/dist/compiler/config/schemas/paths.js +12 -0
- package/dist/compiler/config/schemas/transport/http.d.ts +82 -0
- package/dist/compiler/config/schemas/transport/http.js +33 -0
- package/dist/compiler/config/schemas/transport/stdio.d.ts +9 -0
- package/dist/compiler/config/schemas/transport/stdio.js +15 -0
- package/dist/compiler/config/schemas/webpack.d.ts +3 -0
- package/dist/compiler/config/schemas/webpack.js +15 -0
- package/dist/compiler/config/types.d.ts +1 -0
- package/dist/compiler/config/types.js +2 -0
- package/dist/compiler/config/utils.d.ts +20 -0
- package/dist/compiler/config/utils.js +36 -0
- package/dist/compiler/generate-env-code.d.ts +1 -0
- package/dist/compiler/generate-env-code.js +8 -0
- package/dist/compiler/generate-import-code.d.ts +1 -0
- package/dist/compiler/generate-import-code.js +24 -0
- package/dist/compiler/get-webpack-config/get-entries.d.ts +3 -0
- package/dist/compiler/get-webpack-config/get-entries.js +29 -0
- package/dist/compiler/get-webpack-config/get-externals.d.ts +7 -0
- package/dist/compiler/get-webpack-config/get-externals.js +88 -0
- package/dist/compiler/get-webpack-config/get-injected-variables.d.ts +8 -0
- package/dist/compiler/get-webpack-config/get-injected-variables.js +25 -0
- package/dist/compiler/get-webpack-config/index.d.ts +4 -0
- package/dist/compiler/get-webpack-config/index.js +101 -0
- package/dist/compiler/get-webpack-config/plugins.d.ts +8 -0
- package/dist/compiler/get-webpack-config/plugins.js +132 -0
- package/dist/compiler/get-webpack-config/resolve-tsconfig-paths.d.ts +9 -0
- package/dist/compiler/get-webpack-config/resolve-tsconfig-paths.js +40 -0
- package/dist/compiler/index.d.ts +6 -0
- package/dist/compiler/index.js +194 -0
- package/dist/compiler/on-first-build.d.ts +3 -0
- package/dist/compiler/on-first-build.js +58 -0
- package/dist/compiler/parse-xmcp-config.d.ts +9 -0
- package/dist/compiler/parse-xmcp-config.js +155 -0
- package/dist/compiler/start-http-server.d.ts +1 -0
- package/dist/compiler/start-http-server.js +34 -0
- package/dist/index.d.ts +12 -54
- package/dist/index.js +22 -190
- package/dist/index.js.LICENSE.txt +49 -0
- package/dist/runtime/__tests__/audit.test.d.ts +4 -0
- package/dist/runtime/__tests__/audit.test.js +328 -0
- package/dist/runtime/__tests__/identity.test.d.ts +4 -0
- package/dist/runtime/__tests__/identity.test.js +164 -0
- package/dist/runtime/__tests__/mcpi-runtime.test.d.ts +4 -0
- package/dist/runtime/__tests__/mcpi-runtime.test.js +372 -0
- package/dist/runtime/__tests__/proof.test.d.ts +4 -0
- package/dist/runtime/__tests__/proof.test.js +302 -0
- package/dist/runtime/__tests__/session.test.d.ts +4 -0
- package/dist/runtime/__tests__/session.test.js +254 -0
- package/dist/runtime/__tests__/well-known.test.d.ts +4 -0
- package/dist/runtime/__tests__/well-known.test.js +312 -0
- package/dist/runtime/adapter-express.js +2 -0
- package/dist/runtime/adapter-express.js.LICENSE.txt +252 -0
- package/dist/runtime/adapter-nextjs.js +2 -0
- package/dist/runtime/adapter-nextjs.js.LICENSE.txt +53 -0
- package/dist/runtime/adapters/express/index.d.ts +2 -0
- package/dist/runtime/adapters/express/index.js +48 -0
- package/dist/runtime/adapters/nextjs/index.d.ts +8 -0
- package/dist/runtime/adapters/nextjs/index.js +18 -0
- package/dist/runtime/audit.d.ts +93 -0
- package/dist/runtime/audit.js +212 -0
- package/dist/runtime/debug.d.ts +118 -0
- package/dist/runtime/debug.js +612 -0
- package/dist/runtime/delegation-hooks.d.ts +85 -0
- package/dist/runtime/delegation-hooks.js +116 -0
- package/dist/runtime/demo.d.ts +71 -0
- package/dist/runtime/demo.js +135 -0
- package/dist/runtime/headers.d.ts +1 -0
- package/dist/runtime/headers.js +9 -0
- package/dist/runtime/http.js +2 -0
- package/dist/runtime/http.js.LICENSE.txt +252 -0
- package/dist/runtime/identity.d.ts +105 -0
- package/dist/runtime/identity.js +232 -0
- package/dist/runtime/index.d.ts +16 -0
- package/dist/runtime/index.js +56 -0
- package/dist/runtime/mcpi-runtime.d.ts +164 -0
- package/dist/runtime/mcpi-runtime.js +352 -0
- package/dist/runtime/proof.d.ts +87 -0
- package/dist/runtime/proof.js +223 -0
- package/dist/runtime/session.d.ts +88 -0
- package/dist/runtime/session.js +216 -0
- package/dist/runtime/stdio.js +2 -0
- package/dist/runtime/stdio.js.LICENSE.txt +1 -0
- package/dist/runtime/templates/home.d.ts +2 -0
- package/dist/runtime/templates/home.js +50 -0
- package/dist/runtime/transports/http/base-streamable-http.d.ts +25 -0
- package/dist/runtime/transports/http/base-streamable-http.js +16 -0
- package/dist/runtime/transports/http/http-context.d.ts +9 -0
- package/dist/runtime/transports/http/http-context.js +8 -0
- package/dist/runtime/transports/http/index.d.ts +1 -0
- package/dist/runtime/transports/http/index.js +55 -0
- package/dist/runtime/transports/http/setup-cors.d.ts +4 -0
- package/dist/runtime/transports/http/setup-cors.js +24 -0
- package/dist/runtime/transports/http/stateless-streamable-http.d.ts +39 -0
- package/dist/runtime/transports/http/stateless-streamable-http.js +331 -0
- package/dist/runtime/transports/stdio/index.d.ts +1 -0
- package/dist/runtime/transports/stdio/index.js +51 -0
- package/dist/runtime/utils/server.d.ts +42 -0
- package/dist/runtime/utils/server.js +39 -0
- package/dist/runtime/utils/tools.d.ts +8 -0
- package/dist/runtime/utils/tools.js +115 -0
- package/dist/runtime/verifier-middleware.d.ts +76 -0
- package/dist/runtime/verifier-middleware.js +322 -0
- package/dist/runtime/well-known.d.ts +151 -0
- package/dist/runtime/well-known.js +258 -0
- package/dist/storage/config.d.ts +28 -0
- package/dist/storage/config.js +79 -0
- package/dist/storage/delegation.d.ts +59 -0
- package/dist/storage/delegation.js +130 -0
- package/dist/storage/merkle-verifier.d.ts +84 -0
- package/dist/storage/merkle-verifier.js +261 -0
- package/dist/test/__tests__/nonce-cache-integration.test.d.ts +1 -0
- package/dist/test/__tests__/nonce-cache-integration.test.js +116 -0
- package/dist/test/__tests__/nonce-cache.test.d.ts +1 -0
- package/dist/test/__tests__/nonce-cache.test.js +122 -0
- package/dist/test/__tests__/runtime-integration.test.d.ts +4 -0
- package/dist/test/__tests__/runtime-integration.test.js +192 -0
- package/dist/test/__tests__/test-infrastructure.test.d.ts +4 -0
- package/dist/test/__tests__/test-infrastructure.test.js +178 -0
- package/dist/test/deterministic-keys.d.ts +31 -0
- package/dist/test/deterministic-keys.js +108 -0
- package/dist/test/examples/test-usage-example.d.ts +140 -0
- package/dist/test/examples/test-usage-example.js +175 -0
- package/dist/test/index.d.ts +11 -0
- package/dist/test/index.js +27 -0
- package/dist/test/local-verification.d.ts +28 -0
- package/dist/test/local-verification.js +342 -0
- package/dist/test/mock-identity-provider.d.ts +96 -0
- package/dist/test/mock-identity-provider.js +243 -0
- package/dist/test/runtime-integration.d.ts +63 -0
- package/dist/test/runtime-integration.js +140 -0
- package/dist/test/test-environment.d.ts +26 -0
- package/dist/test/test-environment.js +50 -0
- package/dist/types/declarations.d.ts +1 -0
- package/dist/types/declarations.js +6 -0
- package/dist/types/middleware.d.ts +2 -0
- package/dist/types/middleware.js +2 -0
- package/dist/types/tool.d.ts +80 -0
- package/dist/types/tool.js +2 -0
- package/dist/utils/cli-icons.d.ts +3 -0
- package/dist/utils/cli-icons.js +7 -0
- package/dist/utils/constants.d.ts +6 -0
- package/dist/utils/constants.js +13 -0
- package/dist/utils/context.d.ts +33 -0
- package/dist/utils/context.js +58 -0
- package/dist/utils/file-watcher.d.ts +19 -0
- package/dist/utils/file-watcher.js +49 -0
- package/dist/utils/fs-utils.d.ts +2 -0
- package/dist/utils/fs-utils.js +22 -0
- package/dist/utils/path-validation.d.ts +3 -0
- package/dist/utils/path-validation.js +56 -0
- package/dist/utils/spawn-process.d.ts +9 -0
- package/dist/utils/spawn-process.js +50 -0
- package/dist/utils/subscribable.d.ts +12 -0
- package/dist/utils/subscribable.js +44 -0
- package/package.json +99 -21
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Concurrency tests for nonce cache implementations
|
|
4
|
+
* Tests multi-instance replay prevention and atomic operations
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const vitest_1 = require("vitest");
|
|
8
|
+
const memory_nonce_cache_js_1 = require("../memory-nonce-cache.js");
|
|
9
|
+
const redis_nonce_cache_js_1 = require("../redis-nonce-cache.js");
|
|
10
|
+
const dynamodb_nonce_cache_js_1 = require("../dynamodb-nonce-cache.js");
|
|
11
|
+
const cloudflare_kv_nonce_cache_js_1 = require("../cloudflare-kv-nonce-cache.js");
|
|
12
|
+
// Mock implementations for external services
|
|
13
|
+
const mockRedis = {
|
|
14
|
+
exists: vitest_1.vi.fn(),
|
|
15
|
+
set: vitest_1.vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
const mockDynamoDB = {
|
|
18
|
+
getItem: vitest_1.vi.fn(),
|
|
19
|
+
putItem: vitest_1.vi.fn(),
|
|
20
|
+
};
|
|
21
|
+
const mockKV = {
|
|
22
|
+
get: vitest_1.vi.fn(),
|
|
23
|
+
getWithMetadata: vitest_1.vi.fn(),
|
|
24
|
+
put: vitest_1.vi.fn(),
|
|
25
|
+
delete: vitest_1.vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
(0, vitest_1.describe)("Nonce Cache Concurrency Tests", () => {
|
|
28
|
+
let memoryCache;
|
|
29
|
+
let redisCache;
|
|
30
|
+
let dynamoCache;
|
|
31
|
+
let kvCache;
|
|
32
|
+
(0, vitest_1.beforeEach)(() => {
|
|
33
|
+
vitest_1.vi.clearAllMocks();
|
|
34
|
+
memoryCache = new memory_nonce_cache_js_1.MemoryNonceCache(100);
|
|
35
|
+
redisCache = new redis_nonce_cache_js_1.RedisNonceCache(mockRedis, "test:");
|
|
36
|
+
dynamoCache = new dynamodb_nonce_cache_js_1.DynamoNonceCache(mockDynamoDB, "test-table");
|
|
37
|
+
kvCache = new cloudflare_kv_nonce_cache_js_1.CloudflareKVNonceCache(mockKV, "test:");
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.afterEach)(() => {
|
|
40
|
+
memoryCache.destroy();
|
|
41
|
+
});
|
|
42
|
+
(0, vitest_1.describe)("Memory Cache Concurrency", () => {
|
|
43
|
+
(0, vitest_1.it)("should prevent concurrent duplicate nonce addition", async () => {
|
|
44
|
+
const nonce = "concurrent-memory-nonce";
|
|
45
|
+
const ttl = 60;
|
|
46
|
+
// Simulate concurrent add operations
|
|
47
|
+
const promises = [
|
|
48
|
+
memoryCache.add(nonce, ttl),
|
|
49
|
+
memoryCache.add(nonce, ttl),
|
|
50
|
+
memoryCache.add(nonce, ttl),
|
|
51
|
+
];
|
|
52
|
+
const results = await Promise.allSettled(promises);
|
|
53
|
+
// Exactly one should succeed, others should fail
|
|
54
|
+
const successful = results.filter((r) => r.status === "fulfilled");
|
|
55
|
+
const failed = results.filter((r) => r.status === "rejected");
|
|
56
|
+
(0, vitest_1.expect)(successful).toHaveLength(1);
|
|
57
|
+
(0, vitest_1.expect)(failed).toHaveLength(2);
|
|
58
|
+
// All failures should be due to duplicate nonce
|
|
59
|
+
failed.forEach((result) => {
|
|
60
|
+
if (result.status === "rejected") {
|
|
61
|
+
(0, vitest_1.expect)(result.reason.message).toContain("already exists");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// Nonce should exist after successful addition
|
|
65
|
+
(0, vitest_1.expect)(await memoryCache.has(nonce)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
(0, vitest_1.it)("should handle rapid sequential operations", async () => {
|
|
68
|
+
const baseNonce = "rapid-memory-nonce";
|
|
69
|
+
const operations = [];
|
|
70
|
+
// Create 100 rapid sequential operations
|
|
71
|
+
for (let i = 0; i < 100; i++) {
|
|
72
|
+
operations.push(memoryCache.add(`${baseNonce}-${i}`, 60));
|
|
73
|
+
}
|
|
74
|
+
// All should succeed since they're different nonces
|
|
75
|
+
const results = await Promise.allSettled(operations);
|
|
76
|
+
const successful = results.filter((r) => r.status === "fulfilled");
|
|
77
|
+
(0, vitest_1.expect)(successful).toHaveLength(100);
|
|
78
|
+
// All nonces should exist
|
|
79
|
+
for (let i = 0; i < 100; i++) {
|
|
80
|
+
(0, vitest_1.expect)(await memoryCache.has(`${baseNonce}-${i}`)).toBe(true);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.describe)("Redis Cache Atomicity", () => {
|
|
85
|
+
(0, vitest_1.it)("should use atomic SET NX EX for add operations", async () => {
|
|
86
|
+
const nonce = "atomic-redis-nonce";
|
|
87
|
+
const ttl = 300;
|
|
88
|
+
// Mock successful atomic operation
|
|
89
|
+
mockRedis.set.mockResolvedValue("OK");
|
|
90
|
+
await redisCache.add(nonce, ttl);
|
|
91
|
+
// Verify atomic command was used
|
|
92
|
+
(0, vitest_1.expect)(mockRedis.set).toHaveBeenCalledWith("test:atomic-redis-nonce", "1", "EX", ttl, "NX");
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.it)("should handle concurrent add attempts atomically", async () => {
|
|
95
|
+
const nonce = "concurrent-redis-nonce";
|
|
96
|
+
// First call succeeds, subsequent calls fail
|
|
97
|
+
mockRedis.set.mockResolvedValueOnce("OK").mockResolvedValue(null);
|
|
98
|
+
const promises = [
|
|
99
|
+
redisCache.add(nonce, 60),
|
|
100
|
+
redisCache.add(nonce, 60),
|
|
101
|
+
redisCache.add(nonce, 60),
|
|
102
|
+
];
|
|
103
|
+
const results = await Promise.allSettled(promises);
|
|
104
|
+
// First should succeed, others should fail
|
|
105
|
+
(0, vitest_1.expect)(results[0].status).toBe("fulfilled");
|
|
106
|
+
(0, vitest_1.expect)(results[1].status).toBe("rejected");
|
|
107
|
+
(0, vitest_1.expect)(results[2].status).toBe("rejected");
|
|
108
|
+
// Verify all calls used atomic operation
|
|
109
|
+
(0, vitest_1.expect)(mockRedis.set).toHaveBeenCalledTimes(3);
|
|
110
|
+
mockRedis.set.mock.calls.forEach((call) => {
|
|
111
|
+
(0, vitest_1.expect)(call).toEqual([
|
|
112
|
+
vitest_1.expect.stringContaining(nonce),
|
|
113
|
+
"1",
|
|
114
|
+
"EX",
|
|
115
|
+
60,
|
|
116
|
+
"NX",
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
(0, vitest_1.describe)("DynamoDB Cache Atomicity", () => {
|
|
122
|
+
(0, vitest_1.it)("should use conditional writes for atomicity", async () => {
|
|
123
|
+
const nonce = "atomic-dynamo-nonce";
|
|
124
|
+
const ttl = 300;
|
|
125
|
+
// Mock successful conditional write
|
|
126
|
+
mockDynamoDB.putItem.mockReturnValue({
|
|
127
|
+
promise: () => Promise.resolve({}),
|
|
128
|
+
});
|
|
129
|
+
await dynamoCache.add(nonce, ttl);
|
|
130
|
+
// Verify conditional expression was used
|
|
131
|
+
(0, vitest_1.expect)(mockDynamoDB.putItem).toHaveBeenCalledWith({
|
|
132
|
+
TableName: "test-table",
|
|
133
|
+
Item: {
|
|
134
|
+
nonce: { S: nonce },
|
|
135
|
+
expiresAt: { N: vitest_1.expect.any(String) },
|
|
136
|
+
createdAt: { N: vitest_1.expect.any(String) },
|
|
137
|
+
},
|
|
138
|
+
ConditionExpression: "attribute_not_exists(nonce)",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
(0, vitest_1.it)("should handle concurrent add attempts with conditional writes", async () => {
|
|
142
|
+
const nonce = "concurrent-dynamo-nonce";
|
|
143
|
+
// First call succeeds
|
|
144
|
+
mockDynamoDB.putItem.mockReturnValueOnce({
|
|
145
|
+
promise: () => Promise.resolve({}),
|
|
146
|
+
});
|
|
147
|
+
// Subsequent calls fail with conditional check exception
|
|
148
|
+
const conditionalError = new Error("ConditionalCheckFailedException");
|
|
149
|
+
conditionalError.code = "ConditionalCheckFailedException";
|
|
150
|
+
mockDynamoDB.putItem.mockReturnValue({
|
|
151
|
+
promise: () => Promise.reject(conditionalError),
|
|
152
|
+
});
|
|
153
|
+
const promises = [
|
|
154
|
+
dynamoCache.add(nonce, 60),
|
|
155
|
+
dynamoCache.add(nonce, 60),
|
|
156
|
+
dynamoCache.add(nonce, 60),
|
|
157
|
+
];
|
|
158
|
+
const results = await Promise.allSettled(promises);
|
|
159
|
+
// First should succeed, others should fail
|
|
160
|
+
(0, vitest_1.expect)(results[0].status).toBe("fulfilled");
|
|
161
|
+
(0, vitest_1.expect)(results[1].status).toBe("rejected");
|
|
162
|
+
(0, vitest_1.expect)(results[2].status).toBe("rejected");
|
|
163
|
+
// All failures should be due to conditional check
|
|
164
|
+
results.slice(1).forEach((result) => {
|
|
165
|
+
if (result.status === "rejected") {
|
|
166
|
+
(0, vitest_1.expect)(result.reason.message).toContain("already exists");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
(0, vitest_1.describe)("Cloudflare KV Cache Best-Effort Atomicity", () => {
|
|
172
|
+
(0, vitest_1.it)("should attempt atomicity with getWithMetadata", async () => {
|
|
173
|
+
const nonce = "atomic-kv-nonce";
|
|
174
|
+
// Mock no existing value
|
|
175
|
+
mockKV.getWithMetadata.mockResolvedValue({ value: null, metadata: null });
|
|
176
|
+
mockKV.put.mockResolvedValue(undefined);
|
|
177
|
+
await kvCache.add(nonce, 60);
|
|
178
|
+
(0, vitest_1.expect)(mockKV.getWithMetadata).toHaveBeenCalledWith("test:atomic-kv-nonce");
|
|
179
|
+
(0, vitest_1.expect)(mockKV.put).toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
(0, vitest_1.it)("should detect existing nonces with getWithMetadata", async () => {
|
|
182
|
+
const nonce = "existing-kv-nonce";
|
|
183
|
+
// Mock existing valid nonce
|
|
184
|
+
const futureTime = Date.now() + 50000;
|
|
185
|
+
mockKV.getWithMetadata.mockResolvedValue({
|
|
186
|
+
value: JSON.stringify({
|
|
187
|
+
nonce,
|
|
188
|
+
expiresAt: futureTime,
|
|
189
|
+
createdAt: Date.now(),
|
|
190
|
+
}),
|
|
191
|
+
metadata: null,
|
|
192
|
+
});
|
|
193
|
+
await (0, vitest_1.expect)(kvCache.add(nonce, 60)).rejects.toThrow("already exists - potential replay attack");
|
|
194
|
+
});
|
|
195
|
+
(0, vitest_1.it)("should fall back to basic operations when getWithMetadata unavailable", async () => {
|
|
196
|
+
const nonce = "fallback-kv-nonce";
|
|
197
|
+
// Mock getWithMetadata failure
|
|
198
|
+
mockKV.getWithMetadata.mockRejectedValue(new Error("getWithMetadata is not available"));
|
|
199
|
+
// Mock basic operations
|
|
200
|
+
mockKV.get.mockResolvedValue(null);
|
|
201
|
+
mockKV.put.mockResolvedValue(undefined);
|
|
202
|
+
await kvCache.add(nonce, 60);
|
|
203
|
+
// Should fall back to basic has() check
|
|
204
|
+
(0, vitest_1.expect)(mockKV.get).toHaveBeenCalledWith("test:fallback-kv-nonce");
|
|
205
|
+
(0, vitest_1.expect)(mockKV.put).toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
(0, vitest_1.describe)("Cross-Implementation Consistency", () => {
|
|
209
|
+
const testCases = [
|
|
210
|
+
{ name: "Memory", cache: () => memoryCache },
|
|
211
|
+
{
|
|
212
|
+
name: "Redis",
|
|
213
|
+
cache: () => redisCache,
|
|
214
|
+
setup: () => {
|
|
215
|
+
mockRedis.exists.mockResolvedValue(0);
|
|
216
|
+
mockRedis.set.mockResolvedValue("OK");
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "DynamoDB",
|
|
221
|
+
cache: () => dynamoCache,
|
|
222
|
+
setup: () => {
|
|
223
|
+
mockDynamoDB.getItem.mockReturnValue({
|
|
224
|
+
promise: () => Promise.resolve({}),
|
|
225
|
+
});
|
|
226
|
+
mockDynamoDB.putItem.mockReturnValue({
|
|
227
|
+
promise: () => Promise.resolve({}),
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "Cloudflare KV",
|
|
233
|
+
cache: () => kvCache,
|
|
234
|
+
setup: () => {
|
|
235
|
+
mockKV.get.mockResolvedValue(null);
|
|
236
|
+
mockKV.getWithMetadata.mockResolvedValue({
|
|
237
|
+
value: null,
|
|
238
|
+
metadata: null,
|
|
239
|
+
});
|
|
240
|
+
mockKV.put.mockResolvedValue(undefined);
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
testCases.forEach(({ name, cache, setup }) => {
|
|
245
|
+
(0, vitest_1.it)(`${name} should implement consistent interface`, async () => {
|
|
246
|
+
if (setup)
|
|
247
|
+
setup();
|
|
248
|
+
const cacheInstance = cache();
|
|
249
|
+
const nonce = `interface-test-${name.toLowerCase()}-nonce`;
|
|
250
|
+
// Test interface methods exist and work
|
|
251
|
+
(0, vitest_1.expect)(typeof cacheInstance.has).toBe("function");
|
|
252
|
+
(0, vitest_1.expect)(typeof cacheInstance.add).toBe("function");
|
|
253
|
+
(0, vitest_1.expect)(typeof cacheInstance.cleanup).toBe("function");
|
|
254
|
+
// Test basic flow
|
|
255
|
+
if (name === "Memory") {
|
|
256
|
+
// Only test actual functionality for memory cache
|
|
257
|
+
(0, vitest_1.expect)(await cacheInstance.has(nonce)).toBe(false);
|
|
258
|
+
await cacheInstance.add(nonce, 60);
|
|
259
|
+
(0, vitest_1.expect)(await cacheInstance.has(nonce)).toBe(true);
|
|
260
|
+
}
|
|
261
|
+
// cleanup should not throw
|
|
262
|
+
await (0, vitest_1.expect)(cacheInstance.cleanup()).resolves.not.toThrow();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
(0, vitest_1.describe)("Performance and Stress Testing", () => {
|
|
267
|
+
(0, vitest_1.it)("should handle high-frequency operations on memory cache", async () => {
|
|
268
|
+
const startTime = Date.now();
|
|
269
|
+
const operations = [];
|
|
270
|
+
// Create 1000 rapid operations
|
|
271
|
+
for (let i = 0; i < 1000; i++) {
|
|
272
|
+
operations.push(memoryCache.add(`stress-nonce-${i}`, 60));
|
|
273
|
+
}
|
|
274
|
+
await Promise.all(operations);
|
|
275
|
+
const endTime = Date.now();
|
|
276
|
+
// Should complete within reasonable time (adjust threshold as needed)
|
|
277
|
+
(0, vitest_1.expect)(endTime - startTime).toBeLessThan(1000); // 1 second
|
|
278
|
+
// All nonces should exist
|
|
279
|
+
for (let i = 0; i < 1000; i++) {
|
|
280
|
+
(0, vitest_1.expect)(await memoryCache.has(`stress-nonce-${i}`)).toBe(true);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
(0, vitest_1.it)("should handle mixed read/write operations on memory cache", async () => {
|
|
284
|
+
const nonce = "mixed-ops-nonce";
|
|
285
|
+
// Add initial nonce
|
|
286
|
+
await memoryCache.add(nonce, 60);
|
|
287
|
+
// Create mixed operations
|
|
288
|
+
const operations = [];
|
|
289
|
+
for (let i = 0; i < 100; i++) {
|
|
290
|
+
operations.push(memoryCache.has(nonce));
|
|
291
|
+
operations.push(memoryCache.add(`mixed-${i}`, 60));
|
|
292
|
+
}
|
|
293
|
+
const results = await Promise.allSettled(operations);
|
|
294
|
+
// All has() operations should succeed
|
|
295
|
+
// All add() operations should succeed (different nonces)
|
|
296
|
+
const successful = results.filter((r) => r.status === "fulfilled");
|
|
297
|
+
(0, vitest_1.expect)(successful.length).toBeGreaterThan(150); // Most should succeed
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for DynamoDB Nonce Cache
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const dynamodb_nonce_cache_js_1 = require("../dynamodb-nonce-cache.js");
|
|
8
|
+
// Mock DynamoDB client
|
|
9
|
+
const mockDynamoDB = {
|
|
10
|
+
getItem: vitest_1.vi.fn(),
|
|
11
|
+
putItem: vitest_1.vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
// Helper to create mock DynamoDB responses
|
|
14
|
+
const createMockGetItemResponse = (exists, expired = false) => {
|
|
15
|
+
if (!exists) {
|
|
16
|
+
return { promise: () => Promise.resolve({}) };
|
|
17
|
+
}
|
|
18
|
+
const expiresAt = expired
|
|
19
|
+
? Math.floor(Date.now() / 1000) - 100 // Expired 100 seconds ago
|
|
20
|
+
: Math.floor(Date.now() / 1000) + 300; // Expires in 300 seconds
|
|
21
|
+
return {
|
|
22
|
+
promise: () => Promise.resolve({
|
|
23
|
+
Item: {
|
|
24
|
+
nonce: { S: "test-nonce" },
|
|
25
|
+
expiresAt: { N: expiresAt.toString() },
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const createMockPutItemResponse = (success = true) => {
|
|
31
|
+
if (success) {
|
|
32
|
+
return { promise: () => Promise.resolve({}) };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const error = new Error("ConditionalCheckFailedException");
|
|
36
|
+
error.code = "ConditionalCheckFailedException";
|
|
37
|
+
return { promise: () => Promise.reject(error) };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
(0, vitest_1.describe)("DynamoNonceCache", () => {
|
|
41
|
+
let cache;
|
|
42
|
+
(0, vitest_1.beforeEach)(() => {
|
|
43
|
+
vitest_1.vi.clearAllMocks();
|
|
44
|
+
cache = new dynamodb_nonce_cache_js_1.DynamoNonceCache(mockDynamoDB, "test-nonce-table");
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.describe)("Basic Operations", () => {
|
|
47
|
+
(0, vitest_1.it)("should add and check nonce existence", async () => {
|
|
48
|
+
const nonce = "test-nonce-123";
|
|
49
|
+
// Mock DynamoDB responses
|
|
50
|
+
mockDynamoDB.getItem.mockReturnValue(createMockGetItemResponse(false));
|
|
51
|
+
mockDynamoDB.putItem.mockReturnValue(createMockPutItemResponse(true));
|
|
52
|
+
// Initially should not exist
|
|
53
|
+
(0, vitest_1.expect)(await cache.has(nonce)).toBe(false);
|
|
54
|
+
(0, vitest_1.expect)(mockDynamoDB.getItem).toHaveBeenCalledWith({
|
|
55
|
+
TableName: "test-nonce-table",
|
|
56
|
+
Key: { nonce: { S: nonce } },
|
|
57
|
+
ConsistentRead: true,
|
|
58
|
+
});
|
|
59
|
+
// Add nonce
|
|
60
|
+
await cache.add(nonce, 60);
|
|
61
|
+
(0, vitest_1.expect)(mockDynamoDB.putItem).toHaveBeenCalledWith({
|
|
62
|
+
TableName: "test-nonce-table",
|
|
63
|
+
Item: {
|
|
64
|
+
nonce: { S: nonce },
|
|
65
|
+
expiresAt: { N: vitest_1.expect.any(String) },
|
|
66
|
+
createdAt: { N: vitest_1.expect.any(String) },
|
|
67
|
+
},
|
|
68
|
+
ConditionExpression: "attribute_not_exists(nonce)",
|
|
69
|
+
});
|
|
70
|
+
// Mock that it now exists
|
|
71
|
+
mockDynamoDB.getItem.mockReturnValue(createMockGetItemResponse(true));
|
|
72
|
+
(0, vitest_1.expect)(await cache.has(nonce)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
(0, vitest_1.it)("should prevent duplicate nonce addition", async () => {
|
|
75
|
+
const nonce = "duplicate-nonce";
|
|
76
|
+
// Mock DynamoDB conditional check failure
|
|
77
|
+
mockDynamoDB.putItem.mockReturnValue(createMockPutItemResponse(false));
|
|
78
|
+
// Adding duplicate nonce should throw
|
|
79
|
+
await (0, vitest_1.expect)(cache.add(nonce, 60)).rejects.toThrow("Nonce duplicate-nonce already exists - potential replay attack");
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)("should handle expired nonces correctly", async () => {
|
|
82
|
+
const nonce = "expired-nonce";
|
|
83
|
+
// Mock expired nonce
|
|
84
|
+
mockDynamoDB.getItem.mockReturnValue(createMockGetItemResponse(true, true));
|
|
85
|
+
// Should return false for expired nonce
|
|
86
|
+
(0, vitest_1.expect)(await cache.has(nonce)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.describe)("Error Handling", () => {
|
|
90
|
+
(0, vitest_1.it)("should handle ResourceNotFoundException", async () => {
|
|
91
|
+
const nonce = "missing-table-nonce";
|
|
92
|
+
const error = new Error("Table not found");
|
|
93
|
+
error.code = "ResourceNotFoundException";
|
|
94
|
+
mockDynamoDB.getItem.mockReturnValue({
|
|
95
|
+
promise: () => Promise.reject(error),
|
|
96
|
+
});
|
|
97
|
+
// Should return false for missing table
|
|
98
|
+
(0, vitest_1.expect)(await cache.has(nonce)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
(0, vitest_1.it)("should handle ValidationException on add", async () => {
|
|
101
|
+
const nonce = "validation-error-nonce";
|
|
102
|
+
const error = new Error("Invalid request");
|
|
103
|
+
error.code = "ValidationException";
|
|
104
|
+
mockDynamoDB.putItem.mockReturnValue({
|
|
105
|
+
promise: () => Promise.reject(error),
|
|
106
|
+
});
|
|
107
|
+
await (0, vitest_1.expect)(cache.add(nonce, 60)).rejects.toThrow("Invalid DynamoDB operation");
|
|
108
|
+
});
|
|
109
|
+
(0, vitest_1.it)("should handle ProvisionedThroughputExceededException", async () => {
|
|
110
|
+
const nonce = "throughput-error-nonce";
|
|
111
|
+
const error = new Error("Throughput exceeded");
|
|
112
|
+
error.code = "ProvisionedThroughputExceededException";
|
|
113
|
+
mockDynamoDB.putItem.mockReturnValue({
|
|
114
|
+
promise: () => Promise.reject(error),
|
|
115
|
+
});
|
|
116
|
+
await (0, vitest_1.expect)(cache.add(nonce, 60)).rejects.toThrow("DynamoDB throughput exceeded");
|
|
117
|
+
});
|
|
118
|
+
(0, vitest_1.it)("should propagate unknown errors with context", async () => {
|
|
119
|
+
const nonce = "unknown-error-nonce";
|
|
120
|
+
const error = new Error("Unknown error");
|
|
121
|
+
error.code = "UnknownException";
|
|
122
|
+
mockDynamoDB.getItem.mockReturnValue({
|
|
123
|
+
promise: () => Promise.reject(error),
|
|
124
|
+
});
|
|
125
|
+
await (0, vitest_1.expect)(cache.has(nonce)).rejects.toThrow("Failed to check nonce existence");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
(0, vitest_1.describe)("Atomic Operations", () => {
|
|
129
|
+
(0, vitest_1.it)("should use conditional write for atomicity", async () => {
|
|
130
|
+
const nonce = "atomic-test-nonce";
|
|
131
|
+
const ttl = 300;
|
|
132
|
+
mockDynamoDB.putItem.mockReturnValue(createMockPutItemResponse(true));
|
|
133
|
+
await cache.add(nonce, ttl);
|
|
134
|
+
// Verify conditional expression was used
|
|
135
|
+
(0, vitest_1.expect)(mockDynamoDB.putItem).toHaveBeenCalledWith({
|
|
136
|
+
TableName: "test-nonce-table",
|
|
137
|
+
Item: {
|
|
138
|
+
nonce: { S: nonce },
|
|
139
|
+
expiresAt: { N: vitest_1.expect.any(String) },
|
|
140
|
+
createdAt: { N: vitest_1.expect.any(String) },
|
|
141
|
+
},
|
|
142
|
+
ConditionExpression: "attribute_not_exists(nonce)",
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
(0, vitest_1.it)("should use consistent reads for has() operations", async () => {
|
|
146
|
+
const nonce = "consistent-read-nonce";
|
|
147
|
+
mockDynamoDB.getItem.mockReturnValue(createMockGetItemResponse(false));
|
|
148
|
+
await cache.has(nonce);
|
|
149
|
+
(0, vitest_1.expect)(mockDynamoDB.getItem).toHaveBeenCalledWith({
|
|
150
|
+
TableName: "test-nonce-table",
|
|
151
|
+
Key: { nonce: { S: nonce } },
|
|
152
|
+
ConsistentRead: true,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
(0, vitest_1.describe)("Cleanup", () => {
|
|
157
|
+
(0, vitest_1.it)("should be a no-op since DynamoDB handles TTL", async () => {
|
|
158
|
+
// cleanup() should not call any DynamoDB methods
|
|
159
|
+
await cache.cleanup();
|
|
160
|
+
(0, vitest_1.expect)(mockDynamoDB.getItem).not.toHaveBeenCalled();
|
|
161
|
+
(0, vitest_1.expect)(mockDynamoDB.putItem).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
(0, vitest_1.describe)("Custom Configuration", () => {
|
|
165
|
+
(0, vitest_1.it)("should use custom attribute names", () => {
|
|
166
|
+
const customCache = new dynamodb_nonce_cache_js_1.DynamoNonceCache(mockDynamoDB, "custom-table", "customKey", "customTTL");
|
|
167
|
+
mockDynamoDB.getItem.mockReturnValue(createMockGetItemResponse(false));
|
|
168
|
+
customCache.has("test-nonce");
|
|
169
|
+
(0, vitest_1.expect)(mockDynamoDB.getItem).toHaveBeenCalledWith({
|
|
170
|
+
TableName: "custom-table",
|
|
171
|
+
Key: { customKey: { S: "test-nonce" } },
|
|
172
|
+
ConsistentRead: true,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|