spck 0.3.1

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.
Files changed (155) hide show
  1. package/.oxlintrc.json +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +631 -0
  4. package/bin/cli.js +20 -0
  5. package/bin/validate-cwd.js +41 -0
  6. package/dist/config/__tests__/config.test.d.ts +2 -0
  7. package/dist/config/__tests__/config.test.js +262 -0
  8. package/dist/config/__tests__/credentials.test.d.ts +2 -0
  9. package/dist/config/__tests__/credentials.test.js +360 -0
  10. package/dist/config/config.d.ts +33 -0
  11. package/dist/config/config.js +185 -0
  12. package/dist/config/credentials.d.ts +75 -0
  13. package/dist/config/credentials.js +259 -0
  14. package/dist/config/server-selection.d.ts +40 -0
  15. package/dist/config/server-selection.js +130 -0
  16. package/dist/connection/__tests__/firebase-auth.test.d.ts +2 -0
  17. package/dist/connection/__tests__/firebase-auth.test.js +96 -0
  18. package/dist/connection/__tests__/hmac.test.d.ts +2 -0
  19. package/dist/connection/__tests__/hmac.test.js +372 -0
  20. package/dist/connection/auth.d.ts +13 -0
  21. package/dist/connection/auth.js +91 -0
  22. package/dist/connection/firebase-auth.d.ts +40 -0
  23. package/dist/connection/firebase-auth.js +429 -0
  24. package/dist/connection/hmac.d.ts +24 -0
  25. package/dist/connection/hmac.js +109 -0
  26. package/dist/i18n/index.d.ts +25 -0
  27. package/dist/i18n/index.js +101 -0
  28. package/dist/i18n/locales/en.json +313 -0
  29. package/dist/i18n/locales/es.json +302 -0
  30. package/dist/i18n/locales/fr.json +302 -0
  31. package/dist/i18n/locales/id.json +302 -0
  32. package/dist/i18n/locales/ja.json +302 -0
  33. package/dist/i18n/locales/ko.json +302 -0
  34. package/dist/i18n/locales/locales/en.json +309 -0
  35. package/dist/i18n/locales/locales/es.json +302 -0
  36. package/dist/i18n/locales/locales/fr.json +302 -0
  37. package/dist/i18n/locales/locales/id.json +302 -0
  38. package/dist/i18n/locales/locales/ja.json +302 -0
  39. package/dist/i18n/locales/locales/ko.json +302 -0
  40. package/dist/i18n/locales/locales/pt.json +302 -0
  41. package/dist/i18n/locales/locales/zh-Hans.json +302 -0
  42. package/dist/i18n/locales/pt.json +302 -0
  43. package/dist/i18n/locales/zh-Hans.json +302 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.js +493 -0
  46. package/dist/proxy/ProxyClient.d.ts +125 -0
  47. package/dist/proxy/ProxyClient.js +781 -0
  48. package/dist/proxy/ProxySocketWrapper.d.ts +43 -0
  49. package/dist/proxy/ProxySocketWrapper.js +98 -0
  50. package/dist/proxy/__tests__/ProxyClient.test.d.ts +2 -0
  51. package/dist/proxy/__tests__/ProxyClient.test.js +445 -0
  52. package/dist/proxy/__tests__/ProxySocketWrapper.test.d.ts +2 -0
  53. package/dist/proxy/__tests__/ProxySocketWrapper.test.js +190 -0
  54. package/dist/proxy/__tests__/handshake-validation.test.d.ts +2 -0
  55. package/dist/proxy/__tests__/handshake-validation.test.js +282 -0
  56. package/dist/proxy/__tests__/token-refresh-race.test.d.ts +14 -0
  57. package/dist/proxy/__tests__/token-refresh-race.test.js +173 -0
  58. package/dist/proxy/chunking.d.ts +53 -0
  59. package/dist/proxy/chunking.js +127 -0
  60. package/dist/proxy/handshake-validation.d.ts +21 -0
  61. package/dist/proxy/handshake-validation.js +49 -0
  62. package/dist/rpc/__tests__/router.test.d.ts +2 -0
  63. package/dist/rpc/__tests__/router.test.js +262 -0
  64. package/dist/rpc/router.d.ts +37 -0
  65. package/dist/rpc/router.js +132 -0
  66. package/dist/services/BrowserProxyService.d.ts +13 -0
  67. package/dist/services/BrowserProxyService.js +139 -0
  68. package/dist/services/FilesystemService.d.ts +99 -0
  69. package/dist/services/FilesystemService.js +742 -0
  70. package/dist/services/GitService.d.ts +243 -0
  71. package/dist/services/GitService.js +1439 -0
  72. package/dist/services/SearchService.d.ts +93 -0
  73. package/dist/services/SearchService.js +670 -0
  74. package/dist/services/TerminalService.d.ts +62 -0
  75. package/dist/services/TerminalService.js +337 -0
  76. package/dist/services/__tests__/BrowserProxyService.test.d.ts +2 -0
  77. package/dist/services/__tests__/BrowserProxyService.test.js +145 -0
  78. package/dist/services/__tests__/FilesystemService.test.d.ts +2 -0
  79. package/dist/services/__tests__/FilesystemService.test.js +609 -0
  80. package/dist/services/__tests__/GitService.test.d.ts +2 -0
  81. package/dist/services/__tests__/GitService.test.js +953 -0
  82. package/dist/services/__tests__/SearchService.test.d.ts +2 -0
  83. package/dist/services/__tests__/SearchService.test.js +384 -0
  84. package/dist/services/__tests__/TerminalService.test.d.ts +2 -0
  85. package/dist/services/__tests__/TerminalService.test.js +513 -0
  86. package/dist/setup/wizard.d.ts +10 -0
  87. package/dist/setup/wizard.js +172 -0
  88. package/dist/types.d.ts +196 -0
  89. package/dist/types.js +44 -0
  90. package/dist/utils/__tests__/gitignore.test.d.ts +2 -0
  91. package/dist/utils/__tests__/gitignore.test.js +127 -0
  92. package/dist/utils/gitignore.d.ts +24 -0
  93. package/dist/utils/gitignore.js +77 -0
  94. package/dist/utils/logger.d.ts +96 -0
  95. package/dist/utils/logger.js +456 -0
  96. package/dist/utils/project-dir.d.ts +51 -0
  97. package/dist/utils/project-dir.js +191 -0
  98. package/dist/utils/ripgrep.d.ts +34 -0
  99. package/dist/utils/ripgrep.js +148 -0
  100. package/dist/utils/tool-detection.d.ts +17 -0
  101. package/dist/utils/tool-detection.js +126 -0
  102. package/dist/watcher/FileWatcher.d.ts +10 -0
  103. package/dist/watcher/FileWatcher.js +42 -0
  104. package/package.json +70 -0
  105. package/src/config/__tests__/config.test.ts +318 -0
  106. package/src/config/__tests__/credentials.test.ts +494 -0
  107. package/src/config/config.ts +206 -0
  108. package/src/config/credentials.ts +302 -0
  109. package/src/config/server-selection.ts +150 -0
  110. package/src/connection/__tests__/firebase-auth.test.ts +121 -0
  111. package/src/connection/__tests__/hmac.test.ts +509 -0
  112. package/src/connection/auth.ts +140 -0
  113. package/src/connection/firebase-auth.ts +504 -0
  114. package/src/connection/hmac.ts +139 -0
  115. package/src/i18n/index.ts +119 -0
  116. package/src/i18n/locales/en.json +313 -0
  117. package/src/i18n/locales/es.json +302 -0
  118. package/src/i18n/locales/fr.json +302 -0
  119. package/src/i18n/locales/id.json +302 -0
  120. package/src/i18n/locales/ja.json +302 -0
  121. package/src/i18n/locales/ko.json +302 -0
  122. package/src/i18n/locales/pt.json +302 -0
  123. package/src/i18n/locales/zh-Hans.json +302 -0
  124. package/src/index.ts +542 -0
  125. package/src/proxy/ProxyClient.ts +968 -0
  126. package/src/proxy/ProxySocketWrapper.ts +113 -0
  127. package/src/proxy/__tests__/ProxyClient.test.ts +575 -0
  128. package/src/proxy/__tests__/ProxySocketWrapper.test.ts +251 -0
  129. package/src/proxy/__tests__/handshake-validation.test.ts +367 -0
  130. package/src/proxy/chunking.ts +162 -0
  131. package/src/proxy/handshake-validation.ts +64 -0
  132. package/src/rpc/__tests__/router.test.ts +400 -0
  133. package/src/rpc/router.ts +183 -0
  134. package/src/services/BrowserProxyService.ts +179 -0
  135. package/src/services/FilesystemService.ts +841 -0
  136. package/src/services/GitService.ts +1639 -0
  137. package/src/services/SearchService.ts +809 -0
  138. package/src/services/TerminalService.ts +413 -0
  139. package/src/services/__tests__/BrowserProxyService.test.ts +155 -0
  140. package/src/services/__tests__/FilesystemService.test.ts +1002 -0
  141. package/src/services/__tests__/GitService.test.ts +1552 -0
  142. package/src/services/__tests__/SearchService.test.ts +484 -0
  143. package/src/services/__tests__/TerminalService.test.ts +702 -0
  144. package/src/setup/wizard.ts +242 -0
  145. package/src/types/fossil-delta.d.ts +4 -0
  146. package/src/types.ts +287 -0
  147. package/src/utils/__tests__/gitignore.test.ts +174 -0
  148. package/src/utils/gitignore.ts +91 -0
  149. package/src/utils/logger.ts +578 -0
  150. package/src/utils/project-dir.ts +218 -0
  151. package/src/utils/ripgrep.ts +180 -0
  152. package/src/utils/tool-detection.ts +141 -0
  153. package/src/watcher/FileWatcher.ts +53 -0
  154. package/tsconfig.json +24 -0
  155. package/vitest.config.ts +19 -0
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
2
+ /**
3
+ * Unit tests for ProxySocketWrapper
4
+ */
5
+
6
+ import { ProxySocketWrapper } from '../ProxySocketWrapper.js';
7
+
8
+ describe('ProxySocketWrapper', () => {
9
+ let wrapper: ProxySocketWrapper;
10
+ let mockSendFn: Mock;
11
+ const connectionId = 'test-connection-123';
12
+ const userId = 'test-user-456';
13
+ const deviceId = 'test-device-789';
14
+
15
+ beforeEach(() => {
16
+ mockSendFn = vi.fn();
17
+ wrapper = new ProxySocketWrapper(connectionId, userId, mockSendFn, deviceId);
18
+ });
19
+
20
+ describe('constructor', () => {
21
+ it('should initialize with correct connection ID, user ID, and device ID', () => {
22
+ expect(wrapper.id).toBe(connectionId);
23
+ expect(wrapper.data.uid).toBe(userId);
24
+ expect(wrapper.data.deviceId).toBe(deviceId);
25
+ });
26
+
27
+ it('should initialize broadcast object', () => {
28
+ expect(wrapper.broadcast).toBeDefined();
29
+ expect(typeof wrapper.broadcast.emit).toBe('function');
30
+ });
31
+ });
32
+
33
+ describe('emit()', () => {
34
+ it('should call sendFn with correct parameters', () => {
35
+ const event = 'test-event';
36
+ const data = { message: 'hello' };
37
+
38
+ wrapper.emit(event, data);
39
+
40
+ expect(mockSendFn).toHaveBeenCalledWith(connectionId, event, data);
41
+ });
42
+
43
+ it('should handle emit without data', () => {
44
+ const event = 'test-event';
45
+
46
+ wrapper.emit(event);
47
+
48
+ expect(mockSendFn).toHaveBeenCalledWith(connectionId, event, {});
49
+ });
50
+
51
+ it('should return true', () => {
52
+ const result = wrapper.emit('test-event');
53
+ expect(result).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe('broadcast.emit()', () => {
58
+ it('should call regular emit (sends to single client)', () => {
59
+ const event = 'broadcast-event';
60
+ const data = { type: 'notification' };
61
+
62
+ wrapper.broadcast.emit(event, data);
63
+
64
+ expect(mockSendFn).toHaveBeenCalledWith(connectionId, event, data);
65
+ });
66
+
67
+ it('should return true', () => {
68
+ const result = wrapper.broadcast.emit('test-event');
69
+ expect(result).toBe(true);
70
+ });
71
+ });
72
+
73
+ describe('on()', () => {
74
+ it('should register event listener', () => {
75
+ const listener = vi.fn();
76
+
77
+ wrapper.on('rpc', listener);
78
+
79
+ // Trigger the event
80
+ wrapper.triggerEvent('rpc', { test: 'data' });
81
+
82
+ expect(listener).toHaveBeenCalledWith({ test: 'data' });
83
+ });
84
+
85
+ it('should support multiple listeners for same event', () => {
86
+ const listener1 = vi.fn();
87
+ const listener2 = vi.fn();
88
+
89
+ wrapper.on('rpc', listener1);
90
+ wrapper.on('rpc', listener2);
91
+
92
+ wrapper.triggerEvent('rpc', { test: 'data' });
93
+
94
+ expect(listener1).toHaveBeenCalledWith({ test: 'data' });
95
+ expect(listener2).toHaveBeenCalledWith({ test: 'data' });
96
+ });
97
+
98
+ it('should return this for chaining', () => {
99
+ const listener = vi.fn();
100
+ const result = wrapper.on('test', listener);
101
+ expect(result).toBe(wrapper);
102
+ });
103
+ });
104
+
105
+ describe('off()', () => {
106
+ it('should remove specific listener', () => {
107
+ const listener1 = vi.fn();
108
+ const listener2 = vi.fn();
109
+
110
+ wrapper.on('rpc', listener1);
111
+ wrapper.on('rpc', listener2);
112
+ wrapper.off('rpc', listener1);
113
+
114
+ wrapper.triggerEvent('rpc', { test: 'data' });
115
+
116
+ expect(listener1).not.toHaveBeenCalled();
117
+ expect(listener2).toHaveBeenCalled();
118
+ });
119
+
120
+ it('should remove all listeners for event when no listener specified', () => {
121
+ const listener1 = vi.fn();
122
+ const listener2 = vi.fn();
123
+
124
+ wrapper.on('rpc', listener1);
125
+ wrapper.on('rpc', listener2);
126
+ wrapper.off('rpc');
127
+
128
+ wrapper.triggerEvent('rpc', { test: 'data' });
129
+
130
+ expect(listener1).not.toHaveBeenCalled();
131
+ expect(listener2).not.toHaveBeenCalled();
132
+ });
133
+
134
+ it('should return this for chaining', () => {
135
+ const listener = vi.fn();
136
+ wrapper.on('test', listener);
137
+ const result = wrapper.off('test', listener);
138
+ expect(result).toBe(wrapper);
139
+ });
140
+ });
141
+
142
+ describe('once()', () => {
143
+ it('should trigger listener only once', () => {
144
+ const listener = vi.fn();
145
+
146
+ wrapper.once('rpc', listener);
147
+
148
+ wrapper.triggerEvent('rpc', { call: 1 });
149
+ wrapper.triggerEvent('rpc', { call: 2 });
150
+
151
+ expect(listener).toHaveBeenCalledTimes(1);
152
+ expect(listener).toHaveBeenCalledWith({ call: 1 });
153
+ });
154
+
155
+ it('should return this for chaining', () => {
156
+ const listener = vi.fn();
157
+ const result = wrapper.once('test', listener);
158
+ expect(result).toBe(wrapper);
159
+ });
160
+ });
161
+
162
+ describe('removeAllListeners()', () => {
163
+ it('should remove all listeners for specific event', () => {
164
+ const listener1 = vi.fn();
165
+ const listener2 = vi.fn();
166
+
167
+ wrapper.on('event1', listener1);
168
+ wrapper.on('event2', listener2);
169
+
170
+ wrapper.removeAllListeners('event1');
171
+
172
+ wrapper.triggerEvent('event1', {});
173
+ wrapper.triggerEvent('event2', {});
174
+
175
+ expect(listener1).not.toHaveBeenCalled();
176
+ expect(listener2).toHaveBeenCalled();
177
+ });
178
+
179
+ it('should remove all listeners when no event specified', () => {
180
+ const listener1 = vi.fn();
181
+ const listener2 = vi.fn();
182
+
183
+ wrapper.on('event1', listener1);
184
+ wrapper.on('event2', listener2);
185
+
186
+ wrapper.removeAllListeners();
187
+
188
+ wrapper.triggerEvent('event1', {});
189
+ wrapper.triggerEvent('event2', {});
190
+
191
+ expect(listener1).not.toHaveBeenCalled();
192
+ expect(listener2).not.toHaveBeenCalled();
193
+ });
194
+
195
+ it('should return this for chaining', () => {
196
+ const result = wrapper.removeAllListeners();
197
+ expect(result).toBe(wrapper);
198
+ });
199
+ });
200
+
201
+ describe('triggerEvent()', () => {
202
+ it('should call all registered listeners with arguments', () => {
203
+ const listener = vi.fn();
204
+
205
+ wrapper.on('test', listener);
206
+ wrapper.triggerEvent('test', 'arg1', 'arg2', 'arg3');
207
+
208
+ expect(listener).toHaveBeenCalledWith('arg1', 'arg2', 'arg3');
209
+ });
210
+
211
+ it('should handle errors in listeners gracefully', () => {
212
+ const errorListener = vi.fn(() => {
213
+ throw new Error('Listener error');
214
+ });
215
+ const goodListener = vi.fn();
216
+
217
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
218
+
219
+ wrapper.on('test', errorListener);
220
+ wrapper.on('test', goodListener);
221
+
222
+ wrapper.triggerEvent('test', {});
223
+
224
+ expect(errorListener).toHaveBeenCalled();
225
+ expect(goodListener).toHaveBeenCalled();
226
+ expect(consoleErrorSpy).toHaveBeenCalled();
227
+
228
+ consoleErrorSpy.mockRestore();
229
+ });
230
+
231
+ it('should do nothing if no listeners registered', () => {
232
+ // Should not throw
233
+ expect(() => {
234
+ wrapper.triggerEvent('nonexistent-event', {});
235
+ }).not.toThrow();
236
+ });
237
+ });
238
+
239
+ describe('id getter', () => {
240
+ it('should return connection ID', () => {
241
+ expect(wrapper.id).toBe(connectionId);
242
+ });
243
+ });
244
+
245
+ describe('data property', () => {
246
+ it('should have uid property', () => {
247
+ expect(wrapper.data).toHaveProperty('uid');
248
+ expect(wrapper.data.uid).toBe(userId);
249
+ });
250
+ });
251
+ });
@@ -0,0 +1,367 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ /**
3
+ * Tests for handshake validation - Replay attack prevention
4
+ */
5
+
6
+ import { validateHandshakeTimestamp } from '../handshake-validation.js';
7
+
8
+ describe('validateHandshakeTimestamp - Replay Attack Prevention', () => {
9
+ const ONE_MINUTE = 60 * 1000;
10
+ const NOW = 1640000000000; // Fixed timestamp for testing
11
+
12
+ describe('Valid timestamps', () => {
13
+ it('should accept message with current timestamp', () => {
14
+ const result = validateHandshakeTimestamp(NOW, { now: NOW });
15
+
16
+ expect(result.valid).toBe(true);
17
+ expect(result.error).toBeUndefined();
18
+ });
19
+
20
+ it('should accept message 30 seconds old', () => {
21
+ const timestamp = NOW - 30 * 1000;
22
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
23
+
24
+ expect(result.valid).toBe(true);
25
+ expect(result.error).toBeUndefined();
26
+ });
27
+
28
+ it('should accept message exactly 1 minute old (boundary)', () => {
29
+ const timestamp = NOW - ONE_MINUTE;
30
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
31
+
32
+ expect(result.valid).toBe(true);
33
+ expect(result.error).toBeUndefined();
34
+ });
35
+
36
+ it('should accept message 59 seconds old', () => {
37
+ const timestamp = NOW - 59 * 1000;
38
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
39
+
40
+ expect(result.valid).toBe(true);
41
+ expect(result.error).toBeUndefined();
42
+ });
43
+
44
+ it('should accept message 30 seconds in future (clock skew)', () => {
45
+ const timestamp = NOW + 30 * 1000;
46
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
47
+
48
+ expect(result.valid).toBe(true);
49
+ expect(result.error).toBeUndefined();
50
+ });
51
+
52
+ it('should accept message exactly 1 minute in future (clock skew boundary)', () => {
53
+ const timestamp = NOW + ONE_MINUTE;
54
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
55
+
56
+ expect(result.valid).toBe(true);
57
+ expect(result.error).toBeUndefined();
58
+ });
59
+
60
+ it('should accept message 59 seconds in future', () => {
61
+ const timestamp = NOW + 59 * 1000;
62
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
63
+
64
+ expect(result.valid).toBe(true);
65
+ expect(result.error).toBeUndefined();
66
+ });
67
+ });
68
+
69
+ describe('Invalid timestamps - Too old', () => {
70
+ it('should reject message 2 minutes old', () => {
71
+ const timestamp = NOW - 2 * ONE_MINUTE;
72
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
73
+
74
+ expect(result.valid).toBe(false);
75
+ expect(result.error).toContain('too old');
76
+ expect(result.error).toContain('120s'); // 2 minutes
77
+ });
78
+
79
+ it('should reject message 5 minutes old', () => {
80
+ const timestamp = NOW - 5 * ONE_MINUTE;
81
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
82
+
83
+ expect(result.valid).toBe(false);
84
+ expect(result.error).toContain('too old');
85
+ expect(result.error).toContain('300s'); // 5 minutes
86
+ });
87
+
88
+ it('should reject message 1 minute and 1 millisecond old', () => {
89
+ const timestamp = NOW - ONE_MINUTE - 1;
90
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
91
+
92
+ expect(result.valid).toBe(false);
93
+ expect(result.error).toContain('too old');
94
+ });
95
+
96
+ it('should reject message 61 seconds old', () => {
97
+ const timestamp = NOW - 61 * 1000;
98
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
99
+
100
+ expect(result.valid).toBe(false);
101
+ expect(result.error).toContain('too old');
102
+ });
103
+
104
+ it('should reject very old message (1 hour)', () => {
105
+ const timestamp = NOW - 60 * ONE_MINUTE;
106
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
107
+
108
+ expect(result.valid).toBe(false);
109
+ expect(result.error).toContain('too old');
110
+ });
111
+ });
112
+
113
+ describe('Invalid timestamps - Too far in future', () => {
114
+ it('should reject message 2 minutes in future', () => {
115
+ const timestamp = NOW + 2 * ONE_MINUTE;
116
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
117
+
118
+ expect(result.valid).toBe(false);
119
+ expect(result.error).toContain('too far in future');
120
+ expect(result.error).toContain('120s'); // 2 minutes
121
+ });
122
+
123
+ it('should reject message 5 minutes in future', () => {
124
+ const timestamp = NOW + 5 * ONE_MINUTE;
125
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
126
+
127
+ expect(result.valid).toBe(false);
128
+ expect(result.error).toContain('too far in future');
129
+ });
130
+
131
+ it('should reject message 1 minute and 1 millisecond in future', () => {
132
+ const timestamp = NOW + ONE_MINUTE + 1;
133
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
134
+
135
+ expect(result.valid).toBe(false);
136
+ expect(result.error).toContain('too far in future');
137
+ });
138
+
139
+ it('should reject message 61 seconds in future', () => {
140
+ const timestamp = NOW + 61 * 1000;
141
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
142
+
143
+ expect(result.valid).toBe(false);
144
+ expect(result.error).toContain('too far in future');
145
+ });
146
+ });
147
+
148
+ describe('Invalid timestamp formats', () => {
149
+ it('should reject non-number timestamp', () => {
150
+ const result = validateHandshakeTimestamp('invalid' as any, { now: NOW });
151
+
152
+ expect(result.valid).toBe(false);
153
+ expect(result.error).toContain('Invalid timestamp format');
154
+ });
155
+
156
+ it('should reject NaN timestamp', () => {
157
+ const result = validateHandshakeTimestamp(NaN, { now: NOW });
158
+
159
+ expect(result.valid).toBe(false);
160
+ expect(result.error).toContain('Invalid timestamp format');
161
+ });
162
+
163
+ it('should reject Infinity timestamp', () => {
164
+ const result = validateHandshakeTimestamp(Infinity, { now: NOW });
165
+
166
+ expect(result.valid).toBe(false);
167
+ expect(result.error).toContain('Invalid timestamp format');
168
+ });
169
+
170
+ it('should reject negative timestamp', () => {
171
+ const result = validateHandshakeTimestamp(-1000, { now: NOW });
172
+
173
+ expect(result.valid).toBe(false);
174
+ expect(result.error).toContain('Timestamp must be positive');
175
+ });
176
+
177
+ it('should reject zero timestamp', () => {
178
+ const result = validateHandshakeTimestamp(0, { now: NOW });
179
+
180
+ expect(result.valid).toBe(false);
181
+ expect(result.error).toContain('Timestamp must be positive');
182
+ });
183
+
184
+ it('should reject null timestamp', () => {
185
+ const result = validateHandshakeTimestamp(null as any, { now: NOW });
186
+
187
+ expect(result.valid).toBe(false);
188
+ expect(result.error).toContain('Invalid timestamp format');
189
+ });
190
+
191
+ it('should reject undefined timestamp', () => {
192
+ const result = validateHandshakeTimestamp(undefined as any, { now: NOW });
193
+
194
+ expect(result.valid).toBe(false);
195
+ expect(result.error).toContain('Invalid timestamp format');
196
+ });
197
+ });
198
+
199
+ describe('Custom options', () => {
200
+ it('should accept custom maxAge (5 minutes)', () => {
201
+ const timestamp = NOW - 3 * ONE_MINUTE;
202
+ const result = validateHandshakeTimestamp(timestamp, {
203
+ now: NOW,
204
+ maxAge: 5 * ONE_MINUTE,
205
+ });
206
+
207
+ expect(result.valid).toBe(true);
208
+ });
209
+
210
+ it('should reject with custom maxAge (30 seconds)', () => {
211
+ const timestamp = NOW - 45 * 1000;
212
+ const result = validateHandshakeTimestamp(timestamp, {
213
+ now: NOW,
214
+ maxAge: 30 * 1000,
215
+ });
216
+
217
+ expect(result.valid).toBe(false);
218
+ expect(result.error).toContain('too old');
219
+ });
220
+
221
+ it('should accept custom clockSkewTolerance (2 minutes)', () => {
222
+ const timestamp = NOW + 90 * 1000;
223
+ const result = validateHandshakeTimestamp(timestamp, {
224
+ now: NOW,
225
+ clockSkewTolerance: 2 * ONE_MINUTE,
226
+ });
227
+
228
+ expect(result.valid).toBe(true);
229
+ });
230
+
231
+ it('should reject with custom clockSkewTolerance (30 seconds)', () => {
232
+ const timestamp = NOW + 45 * 1000;
233
+ const result = validateHandshakeTimestamp(timestamp, {
234
+ now: NOW,
235
+ clockSkewTolerance: 30 * 1000,
236
+ });
237
+
238
+ expect(result.valid).toBe(false);
239
+ expect(result.error).toContain('too far in future');
240
+ });
241
+ });
242
+
243
+ describe('Replay attack scenarios', () => {
244
+ it('should prevent replay of 2-minute-old captured message', () => {
245
+ // Simulate attacker capturing a message and replaying it 2 minutes later
246
+ const capturedTimestamp = NOW - 2 * ONE_MINUTE;
247
+ const replayTime = NOW;
248
+
249
+ const result = validateHandshakeTimestamp(capturedTimestamp, { now: replayTime });
250
+
251
+ expect(result.valid).toBe(false);
252
+ expect(result.error).toContain('too old');
253
+ });
254
+
255
+ it('should prevent replay of 5-minute-old captured message', () => {
256
+ // Simulate attacker capturing a message and replaying it 5 minutes later
257
+ const capturedTimestamp = NOW - 5 * ONE_MINUTE;
258
+ const replayTime = NOW;
259
+
260
+ const result = validateHandshakeTimestamp(capturedTimestamp, { now: replayTime });
261
+
262
+ expect(result.valid).toBe(false);
263
+ expect(result.error).toContain('too old');
264
+ });
265
+
266
+ it('should allow legitimate message sent 30 seconds ago', () => {
267
+ // Simulate legitimate slow network (30 second delay)
268
+ const sentTimestamp = NOW - 30 * 1000;
269
+ const receiveTime = NOW;
270
+
271
+ const result = validateHandshakeTimestamp(sentTimestamp, { now: receiveTime });
272
+
273
+ expect(result.valid).toBe(true);
274
+ });
275
+
276
+ it('should prevent attacker from using far-future timestamp', () => {
277
+ // Attacker tries to use timestamp far in future to extend validity
278
+ const attackTimestamp = NOW + 10 * ONE_MINUTE;
279
+ const receiveTime = NOW;
280
+
281
+ const result = validateHandshakeTimestamp(attackTimestamp, { now: receiveTime });
282
+
283
+ expect(result.valid).toBe(false);
284
+ expect(result.error).toContain('too far in future');
285
+ });
286
+ });
287
+
288
+ describe('Edge cases and boundaries', () => {
289
+ it('should handle message at exact maxAge boundary', () => {
290
+ const timestamp = NOW - ONE_MINUTE;
291
+ const result = validateHandshakeTimestamp(timestamp, {
292
+ now: NOW,
293
+ maxAge: ONE_MINUTE,
294
+ });
295
+
296
+ expect(result.valid).toBe(true);
297
+ });
298
+
299
+ it('should handle message at exact clockSkewTolerance boundary', () => {
300
+ const timestamp = NOW + ONE_MINUTE;
301
+ const result = validateHandshakeTimestamp(timestamp, {
302
+ now: NOW,
303
+ clockSkewTolerance: ONE_MINUTE,
304
+ });
305
+
306
+ expect(result.valid).toBe(true);
307
+ });
308
+
309
+ it('should reject message 1ms past maxAge boundary', () => {
310
+ const timestamp = NOW - ONE_MINUTE - 1;
311
+ const result = validateHandshakeTimestamp(timestamp, {
312
+ now: NOW,
313
+ maxAge: ONE_MINUTE,
314
+ });
315
+
316
+ expect(result.valid).toBe(false);
317
+ });
318
+
319
+ it('should reject message 1ms past clockSkewTolerance boundary', () => {
320
+ const timestamp = NOW + ONE_MINUTE + 1;
321
+ const result = validateHandshakeTimestamp(timestamp, {
322
+ now: NOW,
323
+ clockSkewTolerance: ONE_MINUTE,
324
+ });
325
+
326
+ expect(result.valid).toBe(false);
327
+ });
328
+
329
+ it('should handle very recent timestamp (1ms old)', () => {
330
+ const timestamp = NOW - 1;
331
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
332
+
333
+ expect(result.valid).toBe(true);
334
+ });
335
+
336
+ it('should handle very recent future timestamp (1ms ahead)', () => {
337
+ const timestamp = NOW + 1;
338
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
339
+
340
+ expect(result.valid).toBe(true);
341
+ });
342
+ });
343
+
344
+ describe('Error messages', () => {
345
+ it('should provide detailed error for old message', () => {
346
+ const timestamp = NOW - 2 * ONE_MINUTE;
347
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
348
+
349
+ expect(result.error).toContain('age: 120s');
350
+ expect(result.error).toContain('max: 60s');
351
+ });
352
+
353
+ it('should provide detailed error for future message', () => {
354
+ const timestamp = NOW + 2 * ONE_MINUTE;
355
+ const result = validateHandshakeTimestamp(timestamp, { now: NOW });
356
+
357
+ expect(result.error).toContain('skew: 120s');
358
+ expect(result.error).toContain('max: 60s');
359
+ });
360
+
361
+ it('should provide clear error for invalid format', () => {
362
+ const result = validateHandshakeTimestamp('not-a-number' as any, { now: NOW });
363
+
364
+ expect(result.error).toBe('Invalid timestamp format');
365
+ });
366
+ });
367
+ });