@codigoconelmer/driftwatch 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/dist/checker.d.ts +11 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +89 -0
- package/dist/checker.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/scheduler.d.ts +3 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +41 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/snapshot.d.ts +4 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +34 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/telegram.d.ts +4 -0
- package/dist/telegram.d.ts.map +1 -0
- package/dist/telegram.js +39 -0
- package/dist/telegram.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elmer Jacobo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# DriftWatch
|
|
2
|
+
|
|
3
|
+
External API schema drift detector with Telegram alerts. No SDK to install in your apps — runs as a standalone daemon or Docker container, pointing at any HTTP endpoint you want to monitor.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/driftwatch)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://buymeacoffee.com/)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
DriftWatch periodically hits your API endpoints and extracts their response **schema** — keys and types, not values. When the schema changes (keys added, removed, or type changed), it fires a Telegram alert instantly.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
⚠️ Schema drift detected!
|
|
17
|
+
|
|
18
|
+
📌 Cards
|
|
19
|
+
🔗 GET https://api.myapp.com/api/cards
|
|
20
|
+
|
|
21
|
+
➕ Added: data.[].role (string)
|
|
22
|
+
➖ Removed: data.[].position_of (string)
|
|
23
|
+
🔄 Changed: data.[].id number → string
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g driftwatch
|
|
32
|
+
# or
|
|
33
|
+
pnpm add -g driftwatch
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
**1. Initialize config in your project**
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd my-project/
|
|
44
|
+
driftwatch init
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**2. Set up Telegram** (one-time, ~5 min)
|
|
48
|
+
|
|
49
|
+
- Open Telegram → search `@BotFather` → send `/newbot` → copy the token
|
|
50
|
+
- Send any message to your new bot
|
|
51
|
+
- Open `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates` in a browser
|
|
52
|
+
- Copy the `chat.id` value from the JSON response
|
|
53
|
+
|
|
54
|
+
**3. Configure environment**
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cp .env.example .env
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```env
|
|
61
|
+
TELEGRAM_TOKEN=your-bot-token-here
|
|
62
|
+
CHAT_ID=your-chat-id-here
|
|
63
|
+
API_TOKEN=your-api-token-here # optional, only if your API requires auth
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**4. Edit `driftwatch.config.yml`** with your endpoints
|
|
67
|
+
|
|
68
|
+
**5. First run** — creates snapshots, no alerts sent
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
driftwatch check
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**6. Start the daemon**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
driftwatch start
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Config reference
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
telegram:
|
|
86
|
+
bot_token: '${TELEGRAM_TOKEN}'
|
|
87
|
+
chat_id: '${CHAT_ID}'
|
|
88
|
+
|
|
89
|
+
endpoints:
|
|
90
|
+
- name: 'Cards'
|
|
91
|
+
url: 'https://api.myapp.com/api/cards?company_id=123'
|
|
92
|
+
method: GET
|
|
93
|
+
headers:
|
|
94
|
+
Authorization: 'Bearer ${API_TOKEN}'
|
|
95
|
+
Accept: 'application/json'
|
|
96
|
+
interval: '*/5 * * * *'
|
|
97
|
+
|
|
98
|
+
- name: 'Create order'
|
|
99
|
+
url: 'https://api.myapp.com/api/orders'
|
|
100
|
+
method: POST
|
|
101
|
+
headers:
|
|
102
|
+
Authorization: 'Bearer ${API_TOKEN}'
|
|
103
|
+
Content-Type: 'application/json'
|
|
104
|
+
body:
|
|
105
|
+
product_id: 1
|
|
106
|
+
quantity: 1
|
|
107
|
+
interval: '0 * * * *'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
| Field | Required | Description |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `name` | yes | Display name used in alerts and snapshot filenames |
|
|
113
|
+
| `url` | yes | Full URL including query params |
|
|
114
|
+
| `method` | no | HTTP method, defaults to `GET` |
|
|
115
|
+
| `headers` | no | Key/value headers sent with every request |
|
|
116
|
+
| `body` | no | JSON body for POST/PUT/PATCH requests |
|
|
117
|
+
| `interval` | yes | Cron expression (e.g. `*/5 * * * *` = every 5 min) |
|
|
118
|
+
|
|
119
|
+
`${VAR}` values are resolved from `.env` or environment variables.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## CLI
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
driftwatch init # generate driftwatch.config.yml
|
|
127
|
+
driftwatch start # start the daemon (cron-based)
|
|
128
|
+
driftwatch start -c /path/to/cfg.yml # custom config path
|
|
129
|
+
driftwatch check # one-shot check all endpoints now
|
|
130
|
+
driftwatch check -c /path/to/cfg.yml # custom config path
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## How it works
|
|
136
|
+
|
|
137
|
+
1. **First run per endpoint** — saves the response schema to `.driftwatch/snapshots/<name>.json`. No alert sent.
|
|
138
|
+
2. **Subsequent runs** — compares live schema against the snapshot.
|
|
139
|
+
- No change → logs "no drift", no alert.
|
|
140
|
+
- Change detected → sends Telegram alert, updates snapshot as new baseline.
|
|
141
|
+
3. **Schema** is a recursive key+type map. Values are ignored. Arrays are sampled from the first element — nested object structure is preserved.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Docker (self-host on VPS)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
cp .env.example .env
|
|
149
|
+
# fill in your tokens
|
|
150
|
+
|
|
151
|
+
docker compose up -d
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Snapshots persist in a named Docker volume (`driftwatch-snapshots`) so they survive container restarts.
|
|
155
|
+
|
|
156
|
+
To view logs:
|
|
157
|
+
```bash
|
|
158
|
+
docker compose logs -f
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Snapshots
|
|
164
|
+
|
|
165
|
+
Snapshots are stored in `.driftwatch/snapshots/` as JSON files named after each endpoint. They are **gitignored by default** — each environment (local, staging, prod) should build its own baseline on first run.
|
|
166
|
+
|
|
167
|
+
Example snapshot:
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"endpoint": "Cards",
|
|
171
|
+
"url": "https://api.myapp.com/api/cards",
|
|
172
|
+
"capturedAt": "2026-01-15T10:30:00.000Z",
|
|
173
|
+
"schema": {
|
|
174
|
+
"status": "boolean",
|
|
175
|
+
"data": {
|
|
176
|
+
"[]": {
|
|
177
|
+
"id": "string",
|
|
178
|
+
"name": "string",
|
|
179
|
+
"email": "string"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Cron expression reference
|
|
189
|
+
|
|
190
|
+
| Expression | Meaning |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `* * * * *` | Every minute |
|
|
193
|
+
| `*/5 * * * *` | Every 5 minutes |
|
|
194
|
+
| `0 * * * *` | Every hour |
|
|
195
|
+
| `0 9 * * *` | Every day at 9am |
|
|
196
|
+
| `0 9 * * 1` | Every Monday at 9am |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EndpointConfig, SchemaValue, DiffResult } from './types.js';
|
|
2
|
+
export declare function extractSchema(value: unknown, depth?: number): SchemaValue;
|
|
3
|
+
export declare function diffSchemas(prev: SchemaValue, curr: SchemaValue): DiffResult;
|
|
4
|
+
export interface CheckResult {
|
|
5
|
+
endpoint: EndpointConfig;
|
|
6
|
+
isFirstRun: boolean;
|
|
7
|
+
hasDrift: boolean;
|
|
8
|
+
diff: DiffResult;
|
|
9
|
+
}
|
|
10
|
+
export declare function checkEndpoint(endpoint: EndpointConfig): Promise<CheckResult>;
|
|
11
|
+
//# sourceMappingURL=checker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checker.d.ts","sourceRoot":"","sources":["../src/checker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAgB,UAAU,EAA2B,MAAM,YAAY,CAAC;AAIjH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAI,GAAG,WAAW,CAiBpE;AAkBD,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CAuB5E;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,cAAc,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC,CAgClF"}
|
package/dist/checker.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { readSnapshot, writeSnapshot } from './snapshot.js';
|
|
3
|
+
const MAX_DEPTH = 20;
|
|
4
|
+
export function extractSchema(value, depth = 0) {
|
|
5
|
+
if (depth >= MAX_DEPTH)
|
|
6
|
+
return 'object';
|
|
7
|
+
if (value === null)
|
|
8
|
+
return 'null';
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
if (value.length > 0 && value[0] !== null && typeof value[0] === 'object' && !Array.isArray(value[0])) {
|
|
11
|
+
return { '[]': extractSchema(value[0], depth + 1) };
|
|
12
|
+
}
|
|
13
|
+
return 'array';
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === 'object') {
|
|
16
|
+
const obj = {};
|
|
17
|
+
for (const [key, val] of Object.entries(value)) {
|
|
18
|
+
obj[key] = extractSchema(val, depth + 1);
|
|
19
|
+
}
|
|
20
|
+
return obj;
|
|
21
|
+
}
|
|
22
|
+
return typeof value;
|
|
23
|
+
}
|
|
24
|
+
function flattenSchema(schema, prefix = '') {
|
|
25
|
+
if (typeof schema === 'string') {
|
|
26
|
+
return prefix ? { [prefix]: schema } : {};
|
|
27
|
+
}
|
|
28
|
+
const result = {};
|
|
29
|
+
for (const [key, val] of Object.entries(schema)) {
|
|
30
|
+
const p = prefix ? `${prefix}.${key}` : key;
|
|
31
|
+
if (typeof val === 'string') {
|
|
32
|
+
result[p] = val;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
Object.assign(result, flattenSchema(val, p));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
export function diffSchemas(prev, curr) {
|
|
41
|
+
const prevFlat = flattenSchema(prev);
|
|
42
|
+
const currFlat = flattenSchema(curr);
|
|
43
|
+
const added = [];
|
|
44
|
+
const removed = [];
|
|
45
|
+
const changed = [];
|
|
46
|
+
for (const [path, type] of Object.entries(currFlat)) {
|
|
47
|
+
if (!(path in prevFlat)) {
|
|
48
|
+
added.push({ path, type });
|
|
49
|
+
}
|
|
50
|
+
else if (prevFlat[path] !== type) {
|
|
51
|
+
changed.push({ path, from: prevFlat[path], to: type });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const [path, type] of Object.entries(prevFlat)) {
|
|
55
|
+
if (!(path in currFlat)) {
|
|
56
|
+
removed.push({ path, type });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { added, removed, changed };
|
|
60
|
+
}
|
|
61
|
+
export async function checkEndpoint(endpoint) {
|
|
62
|
+
const response = await axios({
|
|
63
|
+
method: endpoint.method,
|
|
64
|
+
url: endpoint.url,
|
|
65
|
+
headers: endpoint.headers ?? {},
|
|
66
|
+
data: endpoint.body,
|
|
67
|
+
timeout: 15000,
|
|
68
|
+
maxContentLength: 5 * 1024 * 1024, // 5MB
|
|
69
|
+
maxBodyLength: 1 * 1024 * 1024, // 1MB request body
|
|
70
|
+
});
|
|
71
|
+
const schema = extractSchema(response.data);
|
|
72
|
+
const snapshot = readSnapshot(endpoint.name);
|
|
73
|
+
if (!snapshot) {
|
|
74
|
+
writeSnapshot(endpoint.name, endpoint.url, schema);
|
|
75
|
+
return {
|
|
76
|
+
endpoint,
|
|
77
|
+
isFirstRun: true,
|
|
78
|
+
hasDrift: false,
|
|
79
|
+
diff: { added: [], removed: [], changed: [] },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const diff = diffSchemas(snapshot.schema, schema);
|
|
83
|
+
const hasDrift = diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
|
|
84
|
+
if (hasDrift) {
|
|
85
|
+
writeSnapshot(endpoint.name, endpoint.url, schema);
|
|
86
|
+
}
|
|
87
|
+
return { endpoint, isFirstRun: false, hasDrift, diff };
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checker.js","sourceRoot":"","sources":["../src/checker.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAiBzC,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAwBtD"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
dotenv.config();
|
|
6
|
+
function resolveEnvVars(value) {
|
|
7
|
+
if (typeof value === 'string') {
|
|
8
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? '');
|
|
9
|
+
}
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.map(resolveEnvVars);
|
|
12
|
+
if (value !== null && typeof value === 'object') {
|
|
13
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveEnvVars(v)]));
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export function loadConfig(configPath) {
|
|
18
|
+
const filePath = configPath ?? path.resolve(process.cwd(), 'driftwatch.config.yml');
|
|
19
|
+
if (!fs.existsSync(filePath)) {
|
|
20
|
+
throw new Error(`Config not found: ${filePath}\nRun "driftwatch init" to create one.`);
|
|
21
|
+
}
|
|
22
|
+
const raw = yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
23
|
+
const resolved = resolveEnvVars(raw);
|
|
24
|
+
if (!resolved.telegram?.bot_token)
|
|
25
|
+
throw new Error('Config missing telegram.bot_token');
|
|
26
|
+
if (!resolved.telegram?.chat_id)
|
|
27
|
+
throw new Error('Config missing telegram.chat_id');
|
|
28
|
+
if (!Array.isArray(resolved.endpoints) || resolved.endpoints.length === 0) {
|
|
29
|
+
throw new Error('Config must have at least one endpoint');
|
|
30
|
+
}
|
|
31
|
+
for (const ep of resolved.endpoints) {
|
|
32
|
+
if (!ep.name)
|
|
33
|
+
throw new Error('Endpoint missing "name"');
|
|
34
|
+
if (!ep.url)
|
|
35
|
+
throw new Error(`Endpoint "${ep.name}" missing "url"`);
|
|
36
|
+
if (!ep.interval)
|
|
37
|
+
throw new Error(`Endpoint "${ep.name}" missing "interval"`);
|
|
38
|
+
if (!ep.method)
|
|
39
|
+
ep.method = 'GET';
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,MAAM,MAAM,QAAQ,CAAC;AAG5B,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC3D,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CACzF,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,UAAmB;IAC5C,MAAM,QAAQ,GAAG,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,uBAAuB,CAAC,CAAC;IAEpF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,wCAAwC,CAAC,CAAC;IACzF,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IACzD,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAW,CAAC;IAE/C,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACxF,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACpF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1E,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;QACpC,IAAI,CAAC,EAAE,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACzD,IAAI,CAAC,EAAE,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC,IAAI,iBAAiB,CAAC,CAAC;QACpE,IAAI,CAAC,EAAE,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC,IAAI,sBAAsB,CAAC,CAAC;QAC9E,IAAI,CAAC,EAAE,CAAC,MAAM;YAAE,EAAE,CAAC,MAAM,GAAG,KAAK,CAAC;IACpC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { initBot, sendDriftAlert } from './telegram.js';
|
|
7
|
+
import { startScheduler } from './scheduler.js';
|
|
8
|
+
import { checkEndpoint } from './checker.js';
|
|
9
|
+
const EXAMPLE_CONFIG = `telegram:
|
|
10
|
+
bot_token: '\${TELEGRAM_TOKEN}'
|
|
11
|
+
chat_id: '\${CHAT_ID}'
|
|
12
|
+
|
|
13
|
+
endpoints:
|
|
14
|
+
- name: 'Users list'
|
|
15
|
+
url: 'https://your-app.com/api/users'
|
|
16
|
+
method: GET
|
|
17
|
+
headers:
|
|
18
|
+
Authorization: 'Bearer \${API_TOKEN}'
|
|
19
|
+
interval: '*/5 * * * *'
|
|
20
|
+
|
|
21
|
+
- name: 'Create order'
|
|
22
|
+
url: 'https://your-app.com/api/orders'
|
|
23
|
+
method: POST
|
|
24
|
+
headers:
|
|
25
|
+
Authorization: 'Bearer \${API_TOKEN}'
|
|
26
|
+
Content-Type: 'application/json'
|
|
27
|
+
body:
|
|
28
|
+
product_id: 1
|
|
29
|
+
quantity: 1
|
|
30
|
+
interval: '0 * * * *'
|
|
31
|
+
`;
|
|
32
|
+
const program = new Command();
|
|
33
|
+
program
|
|
34
|
+
.name('driftwatch')
|
|
35
|
+
.description('External API schema drift detector with Telegram alerts')
|
|
36
|
+
.version('1.0.0');
|
|
37
|
+
program
|
|
38
|
+
.command('init')
|
|
39
|
+
.description('Generate driftwatch.config.yml in the current directory')
|
|
40
|
+
.action(() => {
|
|
41
|
+
const dest = path.resolve(process.cwd(), 'driftwatch.config.yml');
|
|
42
|
+
if (fs.existsSync(dest)) {
|
|
43
|
+
console.log('driftwatch.config.yml already exists.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
fs.writeFileSync(dest, EXAMPLE_CONFIG);
|
|
47
|
+
console.log('Created driftwatch.config.yml — edit it and set your env vars in .env');
|
|
48
|
+
});
|
|
49
|
+
program
|
|
50
|
+
.command('start')
|
|
51
|
+
.description('Start the scheduler daemon')
|
|
52
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
53
|
+
.action((opts) => {
|
|
54
|
+
const config = loadConfig(opts.config);
|
|
55
|
+
initBot(config.telegram.bot_token);
|
|
56
|
+
console.log(`[driftwatch] Starting with ${config.endpoints.length} endpoint(s)...`);
|
|
57
|
+
startScheduler(config);
|
|
58
|
+
});
|
|
59
|
+
program
|
|
60
|
+
.command('check')
|
|
61
|
+
.description('One-shot check all endpoints (no schedule)')
|
|
62
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
63
|
+
.action(async (opts) => {
|
|
64
|
+
const config = loadConfig(opts.config);
|
|
65
|
+
initBot(config.telegram.bot_token);
|
|
66
|
+
console.log(`[driftwatch] Checking ${config.endpoints.length} endpoint(s)...`);
|
|
67
|
+
for (const endpoint of config.endpoints) {
|
|
68
|
+
console.log(`\nChecking: ${endpoint.name}`);
|
|
69
|
+
try {
|
|
70
|
+
const result = await checkEndpoint(endpoint);
|
|
71
|
+
if (result.isFirstRun) {
|
|
72
|
+
console.log(' First run — snapshot saved, no alert.');
|
|
73
|
+
}
|
|
74
|
+
else if (result.hasDrift) {
|
|
75
|
+
const { diff } = result;
|
|
76
|
+
console.log(` Drift detected!`);
|
|
77
|
+
diff.added.forEach(e => console.log(` + ${e.path} (${e.type})`));
|
|
78
|
+
diff.removed.forEach(e => console.log(` - ${e.path} (${e.type})`));
|
|
79
|
+
diff.changed.forEach(e => console.log(` ~ ${e.path}: ${e.from} → ${e.to}`));
|
|
80
|
+
await sendDriftAlert(config.telegram.chat_id, result);
|
|
81
|
+
console.log(' Alert sent.');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(' No drift.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error(` Error: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
program.parse();
|
|
93
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsBtB,CAAC;AAEF,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,YAAY,CAAC;KAClB,WAAW,CAAC,yDAAyD,CAAC;KACtE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,yDAAyD,CAAC;KACtE,MAAM,CAAC,GAAG,EAAE;IACX,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,uBAAuB,CAAC,CAAC;IAClE,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;QACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IACvC,OAAO,CAAC,GAAG,CAAC,uEAAuE,CAAC,CAAC;AACvF,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,4BAA4B,CAAC;KACzC,MAAM,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;KACpD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;IACf,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACnC,OAAO,CAAC,GAAG,CAAC,8BAA8B,MAAM,CAAC,SAAS,CAAC,MAAM,iBAAiB,CAAC,CAAC;IACpF,cAAc,CAAC,MAAM,CAAC,CAAC;AACzB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,4CAA4C,CAAC;KACzD,MAAM,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;KACpD,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;IACrB,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACnC,OAAO,CAAC,GAAG,CAAC,yBAAyB,MAAM,CAAC,SAAS,CAAC,MAAM,iBAAiB,CAAC,CAAC;IAE/E,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,eAAe,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC7C,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;gBACtB,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;YACzD,CAAC;iBAAM,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBAC3B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;gBACxB,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;gBACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;gBACpE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;gBACtE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;gBAC/E,MAAM,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;gBACtD,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,YAAa,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAqCnD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import cron from 'node-cron';
|
|
2
|
+
import { checkEndpoint } from './checker.js';
|
|
3
|
+
import { sendDriftAlert } from './telegram.js';
|
|
4
|
+
export function startScheduler(config) {
|
|
5
|
+
const tasks = [];
|
|
6
|
+
const shutdown = () => {
|
|
7
|
+
console.log('\n[driftwatch] Shutting down...');
|
|
8
|
+
tasks.forEach(t => t.stop());
|
|
9
|
+
process.exit(0);
|
|
10
|
+
};
|
|
11
|
+
process.once('SIGINT', shutdown);
|
|
12
|
+
process.once('SIGTERM', shutdown);
|
|
13
|
+
for (const endpoint of config.endpoints) {
|
|
14
|
+
if (!cron.validate(endpoint.interval)) {
|
|
15
|
+
console.error(`[driftwatch] Invalid cron expression for "${endpoint.name}": ${endpoint.interval}`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const task = cron.schedule(endpoint.interval, async () => {
|
|
19
|
+
console.log(`[driftwatch] Checking ${endpoint.name}...`);
|
|
20
|
+
try {
|
|
21
|
+
const result = await checkEndpoint(endpoint);
|
|
22
|
+
if (result.isFirstRun) {
|
|
23
|
+
console.log(`[driftwatch] ${endpoint.name}: first run, snapshot saved.`);
|
|
24
|
+
}
|
|
25
|
+
else if (result.hasDrift) {
|
|
26
|
+
console.log(`[driftwatch] ${endpoint.name}: drift detected! Sending alert.`);
|
|
27
|
+
await sendDriftAlert(config.telegram.chat_id, result);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log(`[driftwatch] ${endpoint.name}: no drift.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.error(`[driftwatch] Error checking "${endpoint.name}":`, err.message);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
tasks.push(task);
|
|
38
|
+
console.log(`[driftwatch] Scheduled "${endpoint.name}" — ${endpoint.interval}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,KAAK,GAAuC,EAAE,CAAC;IAErD,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;QAC/C,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAElC,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACxC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtC,OAAO,CAAC,KAAK,CAAC,6CAA6C,QAAQ,CAAC,IAAI,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;YACnG,SAAS;QACX,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;YACvD,OAAO,CAAC,GAAG,CAAC,yBAAyB,QAAQ,CAAC,IAAI,KAAK,CAAC,CAAC;YACzD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;gBAC7C,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;oBACtB,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,IAAI,8BAA8B,CAAC,CAAC;gBAC3E,CAAC;qBAAM,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;oBAC3B,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,IAAI,kCAAkC,CAAC,CAAC;oBAC7E,MAAM,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;gBACxD,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,GAAG,CAAC,gBAAgB,QAAQ,CAAC,IAAI,aAAa,CAAC,CAAC;gBAC1D,CAAC;YACH,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,gCAAgC,QAAQ,CAAC,IAAI,IAAI,EAAG,GAAa,CAAC,OAAO,CAAC,CAAC;YAC3F,CAAC;QACH,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEjB,OAAO,CAAC,GAAG,CAAC,2BAA2B,QAAQ,CAAC,IAAI,OAAO,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IAClF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SnapshotFile, SchemaValue } from './types.js';
|
|
2
|
+
export declare function readSnapshot(endpointName: string): SnapshotFile | null;
|
|
3
|
+
export declare function writeSnapshot(endpointName: string, url: string, schema: SchemaValue): void;
|
|
4
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAkB5D,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAItE;AAED,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAY1F"}
|
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const SNAPSHOTS_DIR = path.resolve(process.cwd(), '.driftwatch', 'snapshots');
|
|
4
|
+
function toSlug(name) {
|
|
5
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
6
|
+
}
|
|
7
|
+
function ensureDir() {
|
|
8
|
+
if (!fs.existsSync(SNAPSHOTS_DIR)) {
|
|
9
|
+
fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function snapshotPath(endpointName) {
|
|
13
|
+
return path.join(SNAPSHOTS_DIR, `${toSlug(endpointName)}.json`);
|
|
14
|
+
}
|
|
15
|
+
export function readSnapshot(endpointName) {
|
|
16
|
+
const file = snapshotPath(endpointName);
|
|
17
|
+
if (!fs.existsSync(file))
|
|
18
|
+
return null;
|
|
19
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
export function writeSnapshot(endpointName, url, schema) {
|
|
22
|
+
ensureDir();
|
|
23
|
+
const snapshot = {
|
|
24
|
+
endpoint: endpointName,
|
|
25
|
+
url,
|
|
26
|
+
capturedAt: new Date().toISOString(),
|
|
27
|
+
schema,
|
|
28
|
+
};
|
|
29
|
+
const dest = snapshotPath(endpointName);
|
|
30
|
+
const tmp = `${dest}.tmp`;
|
|
31
|
+
fs.writeFileSync(tmp, JSON.stringify(snapshot, null, 2));
|
|
32
|
+
fs.renameSync(tmp, dest);
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC;AAE9E,SAAS,MAAM,CAAC,IAAY;IAC1B,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,SAAS;IAChB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAClC,EAAE,CAAC,SAAS,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,YAAoB;IAC/C,MAAM,IAAI,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAiB,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,YAAoB,EAAE,GAAW,EAAE,MAAmB;IAClF,SAAS,EAAE,CAAC;IACZ,MAAM,QAAQ,GAAiB;QAC7B,QAAQ,EAAE,YAAY;QACtB,GAAG;QACH,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACpC,MAAM;KACP,CAAC;IACF,MAAM,IAAI,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,GAAG,IAAI,MAAM,CAAC;IAC1B,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACzD,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../src/telegram.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE3C;AAiCD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvF"}
|
package/dist/telegram.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import TelegramBot from 'node-telegram-bot-api';
|
|
2
|
+
let bot = null;
|
|
3
|
+
export function initBot(token) {
|
|
4
|
+
bot = new TelegramBot(token);
|
|
5
|
+
}
|
|
6
|
+
function escapeHtml(str) {
|
|
7
|
+
return str
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"');
|
|
12
|
+
}
|
|
13
|
+
function formatDiff(result) {
|
|
14
|
+
const { endpoint, diff } = result;
|
|
15
|
+
const lines = [
|
|
16
|
+
'⚠️ <b>Schema drift detected!</b>',
|
|
17
|
+
'',
|
|
18
|
+
`📌 ${escapeHtml(endpoint.name)}`,
|
|
19
|
+
`🔗 ${escapeHtml(endpoint.method)} ${escapeHtml(endpoint.url)}`,
|
|
20
|
+
'',
|
|
21
|
+
];
|
|
22
|
+
for (const entry of diff.added) {
|
|
23
|
+
lines.push(`➕ Added: ${escapeHtml(entry.path)} (${escapeHtml(entry.type)})`);
|
|
24
|
+
}
|
|
25
|
+
for (const entry of diff.removed) {
|
|
26
|
+
lines.push(`➖ Removed: ${escapeHtml(entry.path)} (${escapeHtml(entry.type)})`);
|
|
27
|
+
}
|
|
28
|
+
for (const entry of diff.changed) {
|
|
29
|
+
lines.push(`🔄 Changed: ${escapeHtml(entry.path)} ${escapeHtml(entry.from)} → ${escapeHtml(entry.to)}`);
|
|
30
|
+
}
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
|
33
|
+
export async function sendDriftAlert(chatId, result) {
|
|
34
|
+
if (!bot)
|
|
35
|
+
throw new Error('Telegram bot not initialized');
|
|
36
|
+
const message = formatDiff(result);
|
|
37
|
+
await bot.sendMessage(chatId, message, { parse_mode: 'HTML' });
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=telegram.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../src/telegram.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAGhD,IAAI,GAAG,GAAuB,IAAI,CAAC;AAEnC,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,GAAG,GAAG,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG;SACP,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,UAAU,CAAC,MAAmB;IACrC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;IAClC,MAAM,KAAK,GAAa;QACtB,kCAAkC;QAClC,EAAE;QACF,MAAM,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;QACjC,MAAM,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;QAC/D,EAAE;KACH,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,cAAc,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjF,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,cAAc,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjF,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,eAAe,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC3G,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAAc,EAAE,MAAmB;IACtE,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;AACjE,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type SchemaValue = string | SchemaObject;
|
|
2
|
+
export type SchemaObject = {
|
|
3
|
+
[key: string]: SchemaValue;
|
|
4
|
+
};
|
|
5
|
+
export interface EndpointConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
url: string;
|
|
8
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
interval: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TelegramConfig {
|
|
14
|
+
bot_token: string;
|
|
15
|
+
chat_id: string;
|
|
16
|
+
}
|
|
17
|
+
export interface Config {
|
|
18
|
+
telegram: TelegramConfig;
|
|
19
|
+
endpoints: EndpointConfig[];
|
|
20
|
+
}
|
|
21
|
+
export interface SnapshotFile {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
url: string;
|
|
24
|
+
capturedAt: string;
|
|
25
|
+
schema: SchemaValue;
|
|
26
|
+
}
|
|
27
|
+
export interface DiffEntry {
|
|
28
|
+
path: string;
|
|
29
|
+
type: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ChangedEntry {
|
|
32
|
+
path: string;
|
|
33
|
+
from: string;
|
|
34
|
+
to: string;
|
|
35
|
+
}
|
|
36
|
+
export interface DiffResult {
|
|
37
|
+
added: DiffEntry[];
|
|
38
|
+
removed: DiffEntry[];
|
|
39
|
+
changed: ChangedEntry[];
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,YAAY,CAAC;AAChD,MAAM,MAAM,YAAY,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAA;CAAE,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codigoconelmer/driftwatch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "External API schema drift detector with Telegram alerts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"driftwatch": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"prepublishOnly": "pnpm build"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"api",
|
|
23
|
+
"schema",
|
|
24
|
+
"drift",
|
|
25
|
+
"monitor",
|
|
26
|
+
"telegram",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"axios": "^1.7.9",
|
|
32
|
+
"commander": "^12.1.0",
|
|
33
|
+
"dotenv": "^16.4.7",
|
|
34
|
+
"js-yaml": "^4.1.0",
|
|
35
|
+
"node-cron": "^3.0.3",
|
|
36
|
+
"node-telegram-bot-api": "^0.66.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/js-yaml": "^4.0.9",
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"@types/node-cron": "^3.0.11",
|
|
42
|
+
"@types/node-telegram-bot-api": "^0.64.14",
|
|
43
|
+
"tsx": "^4.19.2",
|
|
44
|
+
"typescript": "^5.7.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"pnpm": {
|
|
50
|
+
"onlyBuiltDependencies": ["esbuild"]
|
|
51
|
+
}
|
|
52
|
+
}
|