@dependabit/monitor 0.1.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 (69) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/checkers/github-repo.d.ts +17 -0
  5. package/dist/checkers/github-repo.d.ts.map +1 -0
  6. package/dist/checkers/github-repo.js +115 -0
  7. package/dist/checkers/github-repo.js.map +1 -0
  8. package/dist/checkers/index.d.ts +7 -0
  9. package/dist/checkers/index.d.ts.map +1 -0
  10. package/dist/checkers/index.js +7 -0
  11. package/dist/checkers/index.js.map +1 -0
  12. package/dist/checkers/openapi.d.ts +24 -0
  13. package/dist/checkers/openapi.d.ts.map +1 -0
  14. package/dist/checkers/openapi.js +221 -0
  15. package/dist/checkers/openapi.js.map +1 -0
  16. package/dist/checkers/url-content.d.ts +16 -0
  17. package/dist/checkers/url-content.d.ts.map +1 -0
  18. package/dist/checkers/url-content.js +66 -0
  19. package/dist/checkers/url-content.js.map +1 -0
  20. package/dist/comparator.d.ts +16 -0
  21. package/dist/comparator.d.ts.map +1 -0
  22. package/dist/comparator.js +53 -0
  23. package/dist/comparator.js.map +1 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +12 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/monitor.d.ts +43 -0
  29. package/dist/monitor.d.ts.map +1 -0
  30. package/dist/monitor.js +85 -0
  31. package/dist/monitor.js.map +1 -0
  32. package/dist/normalizer.d.ts +24 -0
  33. package/dist/normalizer.d.ts.map +1 -0
  34. package/dist/normalizer.js +97 -0
  35. package/dist/normalizer.js.map +1 -0
  36. package/dist/scheduler.d.ts +64 -0
  37. package/dist/scheduler.d.ts.map +1 -0
  38. package/dist/scheduler.js +132 -0
  39. package/dist/scheduler.js.map +1 -0
  40. package/dist/severity.d.ts +22 -0
  41. package/dist/severity.d.ts.map +1 -0
  42. package/dist/severity.js +87 -0
  43. package/dist/severity.js.map +1 -0
  44. package/dist/types.d.ts +36 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +5 -0
  47. package/dist/types.js.map +1 -0
  48. package/package.json +39 -0
  49. package/src/checkers/github-repo.ts +150 -0
  50. package/src/checkers/index.ts +7 -0
  51. package/src/checkers/openapi.ts +310 -0
  52. package/src/checkers/url-content.ts +78 -0
  53. package/src/comparator.ts +68 -0
  54. package/src/index.ts +20 -0
  55. package/src/monitor.ts +120 -0
  56. package/src/normalizer.ts +122 -0
  57. package/src/scheduler.ts +175 -0
  58. package/src/severity.ts +112 -0
  59. package/src/types.ts +40 -0
  60. package/test/checkers/github-repo.test.ts +124 -0
  61. package/test/checkers/openapi.test.ts +352 -0
  62. package/test/checkers/url-content.test.ts +99 -0
  63. package/test/comparator.test.ts +108 -0
  64. package/test/monitor.test.ts +177 -0
  65. package/test/normalizer.test.ts +66 -0
  66. package/test/scheduler.test.ts +674 -0
  67. package/test/severity.test.ts +122 -0
  68. package/tsconfig.json +10 -0
  69. package/tsconfig.tsbuildinfo +1 -0
package/src/types.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Common types for dependency checkers
3
+ */
4
+
5
+ export interface DependencySnapshot {
6
+ version?: string | undefined;
7
+ stateHash: string;
8
+ fetchedAt: Date;
9
+ metadata?: Record<string, unknown>;
10
+ }
11
+
12
+ export interface ChangeDetection {
13
+ hasChanged: boolean;
14
+ changes: string[];
15
+ oldVersion?: string | undefined;
16
+ newVersion?: string | undefined;
17
+ diff?: unknown;
18
+ severity?: 'breaking' | 'major' | 'minor';
19
+ }
20
+
21
+ export interface AccessConfig {
22
+ url: string;
23
+ accessMethod: 'github-api' | 'http' | 'openapi' | 'context7' | 'arxiv';
24
+ auth?: {
25
+ type: 'token' | 'basic' | 'oauth' | 'none';
26
+ secret?: string;
27
+ };
28
+ }
29
+
30
+ export interface Checker {
31
+ /**
32
+ * Fetches the current state of a dependency
33
+ */
34
+ fetch(config: AccessConfig): Promise<DependencySnapshot>;
35
+
36
+ /**
37
+ * Compares two snapshots to detect changes
38
+ */
39
+ compare(prev: DependencySnapshot, curr: DependencySnapshot): Promise<ChangeDetection>;
40
+ }
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { GitHubRepoChecker } from '../../src/checkers/github-repo.js';
3
+
4
+ // Mock global fetch
5
+ const mockFetch = vi.fn();
6
+ global.fetch = mockFetch as any;
7
+
8
+ describe('GitHubRepoChecker', () => {
9
+ let checker: GitHubRepoChecker;
10
+
11
+ beforeEach(() => {
12
+ checker = new GitHubRepoChecker();
13
+ vi.clearAllMocks();
14
+
15
+ // Default mock response for releases
16
+ mockFetch.mockResolvedValue({
17
+ ok: true,
18
+ status: 200,
19
+ json: async () => ({
20
+ tag_name: 'v1.0.0',
21
+ name: 'Release v1.0.0',
22
+ published_at: '2024-01-01T00:00:00Z',
23
+ body: 'Release notes',
24
+ html_url: 'https://github.com/owner/repo/releases/v1.0.0'
25
+ })
26
+ });
27
+ });
28
+
29
+ describe('fetch', () => {
30
+ it('should fetch latest release information', async () => {
31
+ const config = {
32
+ url: 'https://github.com/owner/repo',
33
+ accessMethod: 'github-api' as const
34
+ };
35
+
36
+ const result = await checker.fetch(config);
37
+
38
+ expect(result).toBeDefined();
39
+ expect(result.version).toBeDefined();
40
+ expect(result.stateHash).toBeDefined();
41
+ expect(result.fetchedAt).toBeInstanceOf(Date);
42
+ });
43
+
44
+ it('should handle repositories without releases', async () => {
45
+ // Mock 404 response for no releases, then mock commits response
46
+ mockFetch
47
+ .mockResolvedValueOnce({
48
+ ok: false,
49
+ status: 404
50
+ })
51
+ .mockResolvedValueOnce({
52
+ ok: true,
53
+ status: 200,
54
+ json: async () => [
55
+ {
56
+ sha: 'abc123def456',
57
+ commit: {
58
+ message: 'Latest commit',
59
+ author: { date: '2024-01-01T00:00:00Z' }
60
+ }
61
+ }
62
+ ]
63
+ });
64
+
65
+ const config = {
66
+ url: 'https://github.com/owner/no-releases',
67
+ accessMethod: 'github-api' as const
68
+ };
69
+
70
+ const result = await checker.fetch(config);
71
+
72
+ expect(result).toBeDefined();
73
+ expect(result.version).toBeUndefined();
74
+ });
75
+
76
+ it('should throw error for invalid GitHub URL', async () => {
77
+ const config = {
78
+ url: 'https://example.com/not-github',
79
+ accessMethod: 'github-api' as const
80
+ };
81
+
82
+ await expect(checker.fetch(config)).rejects.toThrow();
83
+ });
84
+ });
85
+
86
+ describe('compare', () => {
87
+ it('should detect version changes', async () => {
88
+ const prev = {
89
+ version: 'v1.0.0',
90
+ stateHash: 'hash1',
91
+ fetchedAt: new Date('2024-01-01')
92
+ };
93
+
94
+ const curr = {
95
+ version: 'v1.1.0',
96
+ stateHash: 'hash2',
97
+ fetchedAt: new Date('2024-01-02')
98
+ };
99
+
100
+ const result = await checker.compare(prev, curr);
101
+
102
+ expect(result.hasChanged).toBe(true);
103
+ expect(result.changes).toContain('version');
104
+ });
105
+
106
+ it('should return no change when versions match', async () => {
107
+ const prev = {
108
+ version: 'v1.0.0',
109
+ stateHash: 'hash1',
110
+ fetchedAt: new Date('2024-01-01')
111
+ };
112
+
113
+ const curr = {
114
+ version: 'v1.0.0',
115
+ stateHash: 'hash1',
116
+ fetchedAt: new Date('2024-01-02')
117
+ };
118
+
119
+ const result = await checker.compare(prev, curr);
120
+
121
+ expect(result.hasChanged).toBe(false);
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,352 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { OpenAPIChecker } from '../../src/checkers/openapi.js';
3
+ import type { AccessConfig, DependencySnapshot } from '../../src/types.js';
4
+
5
+ describe('OpenAPIChecker', () => {
6
+ let checker: OpenAPIChecker;
7
+
8
+ beforeEach(() => {
9
+ checker = new OpenAPIChecker();
10
+ });
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ describe('fetch', () => {
17
+ it('should fetch and parse JSON OpenAPI spec', async () => {
18
+ const mockSpec = {
19
+ openapi: '3.0.0',
20
+ info: {
21
+ title: 'Test API',
22
+ version: '1.0.0',
23
+ description: 'A test API'
24
+ },
25
+ paths: {
26
+ '/users': {
27
+ get: { operationId: 'getUsers' },
28
+ post: { operationId: 'createUser' }
29
+ },
30
+ '/users/{id}': {
31
+ get: { operationId: 'getUser' },
32
+ delete: { operationId: 'deleteUser' }
33
+ }
34
+ },
35
+ components: {
36
+ schemas: {
37
+ User: { type: 'object' },
38
+ Error: { type: 'object' }
39
+ }
40
+ }
41
+ };
42
+
43
+ vi.spyOn(global, 'fetch').mockResolvedValue({
44
+ ok: true,
45
+ headers: new Headers({ 'content-type': 'application/json' }),
46
+ text: async () => JSON.stringify(mockSpec)
47
+ } as Response);
48
+
49
+ const config: AccessConfig = {
50
+ url: 'https://api.example.com/openapi.json',
51
+ accessMethod: 'openapi'
52
+ };
53
+
54
+ const snapshot = await checker.fetch(config);
55
+
56
+ expect(snapshot.version).toBe('1.0.0');
57
+ expect(snapshot.stateHash).toBeDefined();
58
+ expect(snapshot.fetchedAt).toBeInstanceOf(Date);
59
+ expect(snapshot.metadata?.title).toBe('Test API');
60
+ expect(snapshot.metadata?.endpointCount).toBe(2);
61
+ expect(snapshot.metadata?.schemaCount).toBe(2);
62
+ });
63
+
64
+ it('should fetch and parse YAML OpenAPI spec', async () => {
65
+ const yamlSpec = `
66
+ openapi: '3.0.0'
67
+ info:
68
+ title: Test API
69
+ version: '2.0.0'
70
+ paths:
71
+ /items:
72
+ get:
73
+ summary: List items
74
+ post:
75
+ summary: Create item
76
+ components:
77
+ schemas:
78
+ Item:
79
+ type: object
80
+ `;
81
+
82
+ vi.spyOn(global, 'fetch').mockResolvedValue({
83
+ ok: true,
84
+ headers: new Headers({ 'content-type': 'text/yaml' }),
85
+ text: async () => yamlSpec
86
+ } as Response);
87
+
88
+ const config: AccessConfig = {
89
+ url: 'https://api.example.com/openapi.yaml',
90
+ accessMethod: 'openapi'
91
+ };
92
+
93
+ const snapshot = await checker.fetch(config);
94
+
95
+ expect(snapshot.version).toBe('2.0.0');
96
+ expect(snapshot.metadata?.title).toBe('Test API');
97
+ });
98
+
99
+ it('should handle authentication token', async () => {
100
+ const mockSpec = { openapi: '3.0.0', info: { version: '1.0.0' } };
101
+ const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue({
102
+ ok: true,
103
+ headers: new Headers({ 'content-type': 'application/json' }),
104
+ text: async () => JSON.stringify(mockSpec)
105
+ } as Response);
106
+
107
+ const config: AccessConfig = {
108
+ url: 'https://api.example.com/openapi.json',
109
+ accessMethod: 'openapi',
110
+ auth: {
111
+ type: 'token',
112
+ secret: 'test-token'
113
+ }
114
+ };
115
+
116
+ await checker.fetch(config);
117
+
118
+ expect(fetchSpy).toHaveBeenCalledWith(
119
+ 'https://api.example.com/openapi.json',
120
+ expect.objectContaining({
121
+ headers: expect.objectContaining({
122
+ Authorization: 'Bearer test-token'
123
+ })
124
+ })
125
+ );
126
+ });
127
+
128
+ it('should throw error on HTTP failure', async () => {
129
+ vi.spyOn(global, 'fetch').mockResolvedValue({
130
+ ok: false,
131
+ status: 404,
132
+ statusText: 'Not Found'
133
+ } as Response);
134
+
135
+ const config: AccessConfig = {
136
+ url: 'https://api.example.com/openapi.json',
137
+ accessMethod: 'openapi'
138
+ };
139
+
140
+ await expect(checker.fetch(config)).rejects.toThrow('HTTP error: 404 Not Found');
141
+ });
142
+
143
+ it('should throw error on invalid JSON', async () => {
144
+ vi.spyOn(global, 'fetch').mockResolvedValue({
145
+ ok: true,
146
+ headers: new Headers({ 'content-type': 'application/json' }),
147
+ text: async () => 'invalid json'
148
+ } as Response);
149
+
150
+ const config: AccessConfig = {
151
+ url: 'https://api.example.com/openapi.json',
152
+ accessMethod: 'openapi'
153
+ };
154
+
155
+ await expect(checker.fetch(config)).rejects.toThrow('Failed to fetch OpenAPI spec');
156
+ });
157
+ });
158
+
159
+ describe('compare', () => {
160
+ it('should detect no changes when specs are identical', async () => {
161
+ const snapshot: DependencySnapshot = {
162
+ version: '1.0.0',
163
+ stateHash: 'abc123',
164
+ fetchedAt: new Date(),
165
+ metadata: {
166
+ endpoints: { '/users': ['GET', 'POST'] },
167
+ schemas: { User: {} }
168
+ }
169
+ };
170
+
171
+ const result = await checker.compare(snapshot, { ...snapshot });
172
+
173
+ expect(result.hasChanged).toBe(false);
174
+ expect(result.changes).toHaveLength(0);
175
+ });
176
+
177
+ it('should detect added endpoints', async () => {
178
+ const prev: DependencySnapshot = {
179
+ version: '1.0.0',
180
+ stateHash: 'abc123',
181
+ fetchedAt: new Date(),
182
+ metadata: {
183
+ endpoints: { '/users': ['GET'] },
184
+ schemas: {}
185
+ }
186
+ };
187
+
188
+ const curr: DependencySnapshot = {
189
+ version: '1.0.0',
190
+ stateHash: 'def456',
191
+ fetchedAt: new Date(),
192
+ metadata: {
193
+ endpoints: { '/users': ['GET'], '/items': ['GET', 'POST'] },
194
+ schemas: {}
195
+ }
196
+ };
197
+
198
+ const result = await checker.compare(prev, curr);
199
+
200
+ expect(result.hasChanged).toBe(true);
201
+ expect(result.changes).toContain('endpoints_added');
202
+ expect(result.severity).toBe('minor');
203
+ });
204
+
205
+ it('should detect removed endpoints as breaking change', async () => {
206
+ const prev: DependencySnapshot = {
207
+ version: '1.0.0',
208
+ stateHash: 'abc123',
209
+ fetchedAt: new Date(),
210
+ metadata: {
211
+ endpoints: { '/users': ['GET'], '/items': ['GET'] },
212
+ schemas: {}
213
+ }
214
+ };
215
+
216
+ const curr: DependencySnapshot = {
217
+ version: '1.0.0',
218
+ stateHash: 'def456',
219
+ fetchedAt: new Date(),
220
+ metadata: {
221
+ endpoints: { '/users': ['GET'] },
222
+ schemas: {}
223
+ }
224
+ };
225
+
226
+ const result = await checker.compare(prev, curr);
227
+
228
+ expect(result.hasChanged).toBe(true);
229
+ expect(result.changes).toContain('endpoints_removed');
230
+ expect(result.severity).toBe('breaking');
231
+ });
232
+
233
+ it('should detect modified endpoints as major change', async () => {
234
+ const prev: DependencySnapshot = {
235
+ version: '1.0.0',
236
+ stateHash: 'abc123',
237
+ fetchedAt: new Date(),
238
+ metadata: {
239
+ endpoints: { '/users': ['GET', 'POST'] },
240
+ schemas: {}
241
+ }
242
+ };
243
+
244
+ const curr: DependencySnapshot = {
245
+ version: '1.0.0',
246
+ stateHash: 'def456',
247
+ fetchedAt: new Date(),
248
+ metadata: {
249
+ endpoints: { '/users': ['GET', 'POST', 'PUT'] },
250
+ schemas: {}
251
+ }
252
+ };
253
+
254
+ const result = await checker.compare(prev, curr);
255
+
256
+ expect(result.hasChanged).toBe(true);
257
+ expect(result.changes).toContain('endpoints_modified');
258
+ expect(result.severity).toBe('major');
259
+ });
260
+
261
+ it('should detect removed schemas as breaking change', async () => {
262
+ const prev: DependencySnapshot = {
263
+ version: '1.0.0',
264
+ stateHash: 'abc123',
265
+ fetchedAt: new Date(),
266
+ metadata: {
267
+ endpoints: {},
268
+ schemas: { User: {}, Item: {} }
269
+ }
270
+ };
271
+
272
+ const curr: DependencySnapshot = {
273
+ version: '1.0.0',
274
+ stateHash: 'def456',
275
+ fetchedAt: new Date(),
276
+ metadata: {
277
+ endpoints: {},
278
+ schemas: { User: {} }
279
+ }
280
+ };
281
+
282
+ const result = await checker.compare(prev, curr);
283
+
284
+ expect(result.hasChanged).toBe(true);
285
+ expect(result.changes).toContain('schemas_removed');
286
+ expect(result.severity).toBe('breaking');
287
+ });
288
+
289
+ it('should detect version changes', async () => {
290
+ const prev: DependencySnapshot = {
291
+ version: '1.0.0',
292
+ stateHash: 'abc123',
293
+ fetchedAt: new Date(),
294
+ metadata: {
295
+ endpoints: {},
296
+ schemas: {}
297
+ }
298
+ };
299
+
300
+ const curr: DependencySnapshot = {
301
+ version: '2.0.0',
302
+ stateHash: 'abc123',
303
+ fetchedAt: new Date(),
304
+ metadata: {
305
+ endpoints: {},
306
+ schemas: {}
307
+ }
308
+ };
309
+
310
+ const result = await checker.compare(prev, curr);
311
+
312
+ expect(result.hasChanged).toBe(true);
313
+ expect(result.changes).toContain('version');
314
+ expect(result.oldVersion).toBe('1.0.0');
315
+ expect(result.newVersion).toBe('2.0.0');
316
+ });
317
+
318
+ it('should include diff details', async () => {
319
+ const prev: DependencySnapshot = {
320
+ version: '1.0.0',
321
+ stateHash: 'abc123',
322
+ fetchedAt: new Date(),
323
+ metadata: {
324
+ endpoints: { '/users': ['GET'] },
325
+ schemas: { User: {} }
326
+ }
327
+ };
328
+
329
+ const curr: DependencySnapshot = {
330
+ version: '2.0.0',
331
+ stateHash: 'def456',
332
+ fetchedAt: new Date(),
333
+ metadata: {
334
+ endpoints: { '/users': ['GET'], '/items': ['POST'] },
335
+ schemas: { User: {}, Item: {} }
336
+ }
337
+ };
338
+
339
+ const result = await checker.compare(prev, curr);
340
+
341
+ expect(result.diff).toBeDefined();
342
+ const diff = result.diff as {
343
+ addedEndpoints: string[];
344
+ addedSchemas: string[];
345
+ versionChanged: boolean;
346
+ };
347
+ expect(diff.addedEndpoints).toContain('/items');
348
+ expect(diff.addedSchemas).toContain('Item');
349
+ expect(diff.versionChanged).toBe(true);
350
+ });
351
+ });
352
+ });
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { URLContentChecker } from '../../src/checkers/url-content.js';
3
+
4
+ // Mock global fetch
5
+ const mockFetch = vi.fn();
6
+ global.fetch = mockFetch as any;
7
+
8
+ describe('URLContentChecker', () => {
9
+ let checker: URLContentChecker;
10
+
11
+ beforeEach(() => {
12
+ checker = new URLContentChecker();
13
+ vi.clearAllMocks();
14
+
15
+ // Default mock response for HTTP content
16
+ mockFetch.mockResolvedValue({
17
+ ok: true,
18
+ status: 200,
19
+ headers: {
20
+ get: (name: string) => (name === 'content-type' ? 'text/html' : null)
21
+ },
22
+ text: async () => '<html><body>Test content</body></html>'
23
+ });
24
+ });
25
+
26
+ describe('fetch', () => {
27
+ it('should fetch and hash URL content', async () => {
28
+ const config = {
29
+ url: 'https://example.com/docs',
30
+ accessMethod: 'http' as const
31
+ };
32
+
33
+ const result = await checker.fetch(config);
34
+
35
+ expect(result).toBeDefined();
36
+ expect(result.stateHash).toBeDefined();
37
+ expect(result.stateHash).toMatch(/^[a-f0-9]{64}$/); // SHA256 hex
38
+ expect(result.fetchedAt).toBeInstanceOf(Date);
39
+ });
40
+
41
+ it('should normalize HTML before hashing', async () => {
42
+ const config = {
43
+ url: 'https://example.com/docs',
44
+ accessMethod: 'http' as const
45
+ };
46
+
47
+ const result = await checker.fetch(config);
48
+
49
+ // Should produce consistent hash after normalization
50
+ expect(result.stateHash).toBeDefined();
51
+ });
52
+
53
+ it('should throw error for unreachable URLs', async () => {
54
+ mockFetch.mockRejectedValue(new Error('Network error'));
55
+
56
+ const config = {
57
+ url: 'https://invalid-url-that-does-not-exist.com',
58
+ accessMethod: 'http' as const
59
+ };
60
+
61
+ await expect(checker.fetch(config)).rejects.toThrow();
62
+ });
63
+ });
64
+
65
+ describe('compare', () => {
66
+ it('should detect content changes via hash difference', async () => {
67
+ const prev = {
68
+ stateHash: 'abc123def456',
69
+ fetchedAt: new Date('2024-01-01')
70
+ };
71
+
72
+ const curr = {
73
+ stateHash: 'xyz789uvw012',
74
+ fetchedAt: new Date('2024-01-02')
75
+ };
76
+
77
+ const result = await checker.compare(prev, curr);
78
+
79
+ expect(result.hasChanged).toBe(true);
80
+ expect(result.changes).toContain('content');
81
+ });
82
+
83
+ it('should return no change when hashes match', async () => {
84
+ const prev = {
85
+ stateHash: 'abc123def456',
86
+ fetchedAt: new Date('2024-01-01')
87
+ };
88
+
89
+ const curr = {
90
+ stateHash: 'abc123def456',
91
+ fetchedAt: new Date('2024-01-02')
92
+ };
93
+
94
+ const result = await checker.compare(prev, curr);
95
+
96
+ expect(result.hasChanged).toBe(false);
97
+ });
98
+ });
99
+ });