@goxtechnologies/connectwise-psa-mcp 1.3.0 → 1.4.2

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 (58) hide show
  1. package/data/reports.json +29 -0
  2. package/dist/index.js +4 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/operations/analytics-extended.d.ts.map +1 -1
  5. package/dist/operations/analytics-extended.js +15 -4
  6. package/dist/operations/analytics-extended.js.map +1 -1
  7. package/dist/operations/analytics-msp-schedule.js +12 -2
  8. package/dist/operations/analytics-msp-schedule.js.map +1 -1
  9. package/dist/operations/analytics-msp-time-entry.d.ts +68 -0
  10. package/dist/operations/analytics-msp-time-entry.d.ts.map +1 -0
  11. package/dist/operations/analytics-msp-time-entry.js +235 -0
  12. package/dist/operations/analytics-msp-time-entry.js.map +1 -0
  13. package/dist/operations/analytics-msp-time.js +27 -8
  14. package/dist/operations/analytics-msp-time.js.map +1 -1
  15. package/dist/operations/analytics.d.ts.map +1 -1
  16. package/dist/operations/analytics.js +2 -0
  17. package/dist/operations/analytics.js.map +1 -1
  18. package/dist/operations/registry.d.ts.map +1 -1
  19. package/dist/operations/registry.js +1 -0
  20. package/dist/operations/registry.js.map +1 -1
  21. package/dist/scrapers/index.d.ts +4 -0
  22. package/dist/scrapers/index.d.ts.map +1 -0
  23. package/dist/scrapers/index.js +10 -0
  24. package/dist/scrapers/index.js.map +1 -0
  25. package/dist/scrapers/reports.d.ts +3 -0
  26. package/dist/scrapers/reports.d.ts.map +1 -0
  27. package/dist/scrapers/reports.js +443 -0
  28. package/dist/scrapers/reports.js.map +1 -0
  29. package/dist/services/load-env.d.ts.map +1 -1
  30. package/dist/services/load-env.js +11 -1
  31. package/dist/services/load-env.js.map +1 -1
  32. package/dist/services/report-cache.d.ts +16 -0
  33. package/dist/services/report-cache.d.ts.map +1 -0
  34. package/dist/services/report-cache.js +230 -0
  35. package/dist/services/report-cache.js.map +1 -0
  36. package/dist/services/report-config.d.ts +28 -0
  37. package/dist/services/report-config.d.ts.map +1 -0
  38. package/dist/services/report-config.js +127 -0
  39. package/dist/services/report-config.js.map +1 -0
  40. package/dist/services/totp.d.ts +27 -0
  41. package/dist/services/totp.d.ts.map +1 -0
  42. package/dist/services/totp.js +76 -0
  43. package/dist/services/totp.js.map +1 -0
  44. package/dist/services/web-scraper.d.ts +76 -0
  45. package/dist/services/web-scraper.d.ts.map +1 -0
  46. package/dist/services/web-scraper.js +435 -0
  47. package/dist/services/web-scraper.js.map +1 -0
  48. package/dist/tools/reports.d.ts +9 -0
  49. package/dist/tools/reports.d.ts.map +1 -0
  50. package/dist/tools/reports.js +388 -0
  51. package/dist/tools/reports.js.map +1 -0
  52. package/dist/tools/validation.js +2 -2
  53. package/dist/tools/validation.js.map +1 -1
  54. package/dist/types/reports.d.ts +87 -0
  55. package/dist/types/reports.d.ts.map +1 -0
  56. package/dist/types/reports.js +3 -0
  57. package/dist/types/reports.js.map +1 -0
  58. package/package.json +18 -4
@@ -0,0 +1,230 @@
1
+ // ConnectWise PSA MCP Server — SQLite-backed Report Cache
2
+ import { fileURLToPath } from 'url';
3
+ import { join, dirname } from 'path';
4
+ import { createHash } from 'crypto';
5
+ import Database from 'better-sqlite3';
6
+ // ---------------------------------------------------------------------------
7
+ // Default DB path — resolves to data/report-cache.db relative to package root
8
+ // ---------------------------------------------------------------------------
9
+ function defaultDbPath() {
10
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
11
+ return join(scriptDir, '..', '..', 'data', 'report-cache.db');
12
+ }
13
+ // ---------------------------------------------------------------------------
14
+ // ReportCache
15
+ // ---------------------------------------------------------------------------
16
+ export class ReportCache {
17
+ db;
18
+ constructor(dbPath) {
19
+ const resolved = dbPath ?? defaultDbPath();
20
+ this.db = new Database(resolved);
21
+ this.initialize();
22
+ }
23
+ // -------------------------------------------------------------------------
24
+ // Initialization — apply PRAGMAs then run DDL
25
+ // -------------------------------------------------------------------------
26
+ initialize() {
27
+ // PRAGMAs that affect the connection must be set via pragma(), not exec()
28
+ this.db.pragma('journal_mode = WAL');
29
+ this.db.pragma('synchronous = NORMAL');
30
+ this.db.pragma('cache_size = -16000');
31
+ this.db.pragma('temp_store = MEMORY');
32
+ // Create tables & indexes — exec() handles multi-statement DDL
33
+ this.db.exec(`
34
+ CREATE TABLE IF NOT EXISTS report_cache (
35
+ id INTEGER PRIMARY KEY,
36
+ report_id TEXT NOT NULL,
37
+ params_hash TEXT NOT NULL,
38
+ params_json TEXT NOT NULL CHECK(json_valid(params_json)),
39
+ data TEXT NOT NULL CHECK(json_valid(data)),
40
+ fetched_at INTEGER NOT NULL,
41
+ source TEXT NOT NULL CHECK(source IN ('xhr', 'export', 'dom')),
42
+ UNIQUE(report_id, params_hash)
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS report_history (
46
+ id INTEGER PRIMARY KEY,
47
+ report_id TEXT NOT NULL,
48
+ params_hash TEXT NOT NULL,
49
+ params_json TEXT NOT NULL CHECK(json_valid(params_json)),
50
+ data TEXT NOT NULL CHECK(json_valid(data)),
51
+ fetched_at INTEGER NOT NULL,
52
+ source TEXT NOT NULL CHECK(source IN ('xhr', 'export', 'dom'))
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS idx_history_report_fetched ON report_history(report_id, fetched_at DESC);
56
+ CREATE INDEX IF NOT EXISTS idx_history_fetched_at ON report_history(fetched_at);
57
+
58
+ CREATE TABLE IF NOT EXISTS calibration_log (
59
+ id INTEGER PRIMARY KEY,
60
+ report_id TEXT NOT NULL,
61
+ params_hash TEXT NOT NULL,
62
+ params_json TEXT NOT NULL CHECK(json_valid(params_json)),
63
+ web_value TEXT NOT NULL,
64
+ api_value TEXT NOT NULL,
65
+ match_type TEXT NOT NULL DEFAULT 'scalar' CHECK(match_type IN ('scalar', 'row_count', 'full_compare')),
66
+ drift REAL,
67
+ logged_at INTEGER NOT NULL
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_calibration_report_logged ON calibration_log(report_id, logged_at DESC);
71
+ `);
72
+ }
73
+ // -------------------------------------------------------------------------
74
+ // hashParams — deep-sort keys, SHA-256, first 16 hex chars
75
+ // -------------------------------------------------------------------------
76
+ hashParams(params) {
77
+ const normalized = sortKeysDeep(params);
78
+ const json = JSON.stringify(normalized);
79
+ return createHash('sha256').update(json).digest('hex').slice(0, 16);
80
+ }
81
+ // -------------------------------------------------------------------------
82
+ // get — lookup by (report_id, params_hash), enforce TTL at read time
83
+ // -------------------------------------------------------------------------
84
+ get(reportId, paramsHash, ttlMinutes) {
85
+ const row = this.db
86
+ .prepare('SELECT * FROM report_cache WHERE report_id = ? AND params_hash = ?')
87
+ .get(reportId, paramsHash);
88
+ if (!row)
89
+ return null;
90
+ const expiresAt = row.fetched_at + ttlMinutes * 60 * 1000;
91
+ if (Date.now() >= expiresAt)
92
+ return null;
93
+ return {
94
+ report_id: row.report_id,
95
+ params_hash: row.params_hash,
96
+ params_json: row.params_json,
97
+ data: JSON.parse(row.data),
98
+ fetched_at: row.fetched_at,
99
+ source: row.source,
100
+ };
101
+ }
102
+ // -------------------------------------------------------------------------
103
+ // set — upsert cache + append history in a single transaction
104
+ // -------------------------------------------------------------------------
105
+ set(reportId, params, data, source) {
106
+ const paramsHash = this.hashParams(params);
107
+ const paramsJson = JSON.stringify(params);
108
+ const dataJson = JSON.stringify(data);
109
+ const now = Date.now();
110
+ const upsert = this.db.prepare(`
111
+ INSERT INTO report_cache (report_id, params_hash, params_json, data, fetched_at, source)
112
+ VALUES (?, ?, ?, ?, ?, ?)
113
+ ON CONFLICT(report_id, params_hash) DO UPDATE SET
114
+ params_json = excluded.params_json,
115
+ data = excluded.data,
116
+ fetched_at = excluded.fetched_at,
117
+ source = excluded.source
118
+ `);
119
+ const appendHistory = this.db.prepare(`
120
+ INSERT INTO report_history (report_id, params_hash, params_json, data, fetched_at, source)
121
+ VALUES (?, ?, ?, ?, ?, ?)
122
+ `);
123
+ const tx = this.db.transaction(() => {
124
+ upsert.run(reportId, paramsHash, paramsJson, dataJson, now, source);
125
+ appendHistory.run(reportId, paramsHash, paramsJson, dataJson, now, source);
126
+ });
127
+ tx();
128
+ }
129
+ // -------------------------------------------------------------------------
130
+ // invalidate — delete from cache by report_id, or all if no arg
131
+ // -------------------------------------------------------------------------
132
+ invalidate(reportId) {
133
+ if (reportId !== undefined) {
134
+ this.db.prepare('DELETE FROM report_cache WHERE report_id = ?').run(reportId);
135
+ }
136
+ else {
137
+ this.db.prepare('DELETE FROM report_cache').run();
138
+ }
139
+ }
140
+ // -------------------------------------------------------------------------
141
+ // getHistory — ordered by fetched_at DESC
142
+ // -------------------------------------------------------------------------
143
+ getHistory(reportId, limit = 50) {
144
+ const rows = this.db
145
+ .prepare('SELECT * FROM report_history WHERE report_id = ? ORDER BY fetched_at DESC, id DESC LIMIT ?')
146
+ .all(reportId, limit);
147
+ return rows.map((row) => ({
148
+ id: row.id,
149
+ report_id: row.report_id,
150
+ params_hash: row.params_hash,
151
+ params_json: row.params_json,
152
+ data: JSON.parse(row.data),
153
+ fetched_at: row.fetched_at,
154
+ source: row.source,
155
+ }));
156
+ }
157
+ // -------------------------------------------------------------------------
158
+ // purgeHistory — delete rows older than retention cutoff, return count
159
+ // -------------------------------------------------------------------------
160
+ purgeHistory(retentionDays) {
161
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
162
+ const result = this.db
163
+ .prepare('DELETE FROM report_history WHERE fetched_at <= ?')
164
+ .run(cutoff);
165
+ return result.changes;
166
+ }
167
+ // -------------------------------------------------------------------------
168
+ // logCalibration — insert into calibration_log
169
+ // -------------------------------------------------------------------------
170
+ logCalibration(reportId, params, webValue, apiValue, matchType, drift) {
171
+ const paramsHash = this.hashParams(params);
172
+ const paramsJson = JSON.stringify(params);
173
+ const now = Date.now();
174
+ this.db
175
+ .prepare(`
176
+ INSERT INTO calibration_log
177
+ (report_id, params_hash, params_json, web_value, api_value, match_type, drift, logged_at)
178
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
179
+ `)
180
+ .run(reportId, paramsHash, paramsJson, webValue, apiValue, matchType, drift ?? null, now);
181
+ }
182
+ // -------------------------------------------------------------------------
183
+ // getCalibrationLog — ordered by logged_at DESC
184
+ // -------------------------------------------------------------------------
185
+ getCalibrationLog(reportId, limit = 50) {
186
+ const rows = this.db
187
+ .prepare('SELECT * FROM calibration_log WHERE report_id = ? ORDER BY logged_at DESC LIMIT ?')
188
+ .all(reportId, limit);
189
+ return rows.map((row) => ({
190
+ id: row.id,
191
+ report_id: row.report_id,
192
+ params_hash: row.params_hash,
193
+ params_json: row.params_json,
194
+ web_value: row.web_value,
195
+ api_value: row.api_value,
196
+ match_type: row.match_type,
197
+ drift: row.drift,
198
+ logged_at: row.logged_at,
199
+ }));
200
+ }
201
+ // -------------------------------------------------------------------------
202
+ // close
203
+ // -------------------------------------------------------------------------
204
+ close() {
205
+ this.db.close();
206
+ }
207
+ }
208
+ // ---------------------------------------------------------------------------
209
+ // Private helpers
210
+ // ---------------------------------------------------------------------------
211
+ /**
212
+ * Deep-sorts an object's keys recursively so that hashing is order-independent.
213
+ * Arrays are left in their original order (element order is semantically significant).
214
+ */
215
+ function sortKeysDeep(value) {
216
+ if (Array.isArray(value)) {
217
+ return value.map(sortKeysDeep);
218
+ }
219
+ if (value !== null && typeof value === 'object') {
220
+ const obj = value;
221
+ return Object.keys(obj)
222
+ .sort()
223
+ .reduce((acc, key) => {
224
+ acc[key] = sortKeysDeep(obj[key]);
225
+ return acc;
226
+ }, {});
227
+ }
228
+ return value;
229
+ }
230
+ //# sourceMappingURL=report-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-cache.js","sourceRoot":"","sources":["../../src/services/report-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { ReportConfig } from '../types/reports.js';
2
+ /**
3
+ * Load the report configuration from `data/reports.json`, optionally
4
+ * deep-merged with a local override file found at:
5
+ * 1. ~/.config/connectwise-psa/reports.local.json (primary)
6
+ * 2. <package_root>/reports.local.json (fallback)
7
+ */
8
+ export declare function loadReportConfig(): ReportConfig;
9
+ /**
10
+ * Deep-merge local overrides into the base config.
11
+ * - Reports are matched by `id`.
12
+ * - Matching reports: local fields override base fields (shallow spread — arrays replaced).
13
+ * - New reports in local (no matching id in base) are appended.
14
+ */
15
+ export declare function mergeLocalOverrides(base: ReportConfig, local: Partial<ReportConfig>): ReportConfig;
16
+ /**
17
+ * Replace `{{var}}` placeholders in a template string with values from the
18
+ * vars map. Unknown placeholders are left as-is.
19
+ */
20
+ export declare function resolveTemplateVars(template: string, vars: Record<string, string>): string;
21
+ /**
22
+ * Derive standard template variables from environment:
23
+ * - `cw_base` — origin of CW_API_URL (e.g. https://na.myconnectwise.net)
24
+ * - `cw_version` — CW_WEB_VERSION env var (default: v2025_1)
25
+ * - `cw_locale` — CW_LOCALE env var (default: en_CA)
26
+ */
27
+ export declare function getTemplateVars(): Record<string, string>;
28
+ //# sourceMappingURL=report-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-config.d.ts","sourceRoot":"","sources":["../../src/services/report-config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,qBAAqB,CAAC;AAkB1E;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,YAAY,CAwB/C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAC3B,YAAY,CAoBd;AAkCD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,MAAM,CAIR;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBxD"}
@@ -0,0 +1,127 @@
1
+ // ConnectWise PSA MCP Server — Report Config Loader
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { dirname, join, resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ // ---------------------------------------------------------------------------
6
+ // Module-level path resolution (same pattern as fast-memory.ts)
7
+ // ---------------------------------------------------------------------------
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ /** Resolve the package root directory. When built, __dirname is dist/services/ */
11
+ function getPackageRoot() {
12
+ return process.env['CLAUDE_PLUGIN_ROOT'] ?? resolve(__dirname, '../..');
13
+ }
14
+ // ---------------------------------------------------------------------------
15
+ // Public API
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * Load the report configuration from `data/reports.json`, optionally
19
+ * deep-merged with a local override file found at:
20
+ * 1. ~/.config/connectwise-psa/reports.local.json (primary)
21
+ * 2. <package_root>/reports.local.json (fallback)
22
+ */
23
+ export function loadReportConfig() {
24
+ // Shipped config — resolved relative to compiled module (dist/services/ → ../../data/)
25
+ const shippedPath = join(__dirname, '..', '..', 'data', 'reports.json');
26
+ const base = JSON.parse(readFileSync(shippedPath, 'utf-8'));
27
+ // Search for local override file
28
+ const homeDir = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
29
+ const overrideCandidates = [
30
+ join(homeDir, '.config', 'connectwise-psa', 'reports.local.json'),
31
+ join(getPackageRoot(), 'reports.local.json'),
32
+ ];
33
+ const localPath = overrideCandidates.find((p) => existsSync(p));
34
+ if (!localPath)
35
+ return base;
36
+ let local;
37
+ try {
38
+ local = JSON.parse(readFileSync(localPath, 'utf-8'));
39
+ }
40
+ catch {
41
+ // Malformed local file — silently skip
42
+ return base;
43
+ }
44
+ return mergeLocalOverrides(base, local);
45
+ }
46
+ /**
47
+ * Deep-merge local overrides into the base config.
48
+ * - Reports are matched by `id`.
49
+ * - Matching reports: local fields override base fields (shallow spread — arrays replaced).
50
+ * - New reports in local (no matching id in base) are appended.
51
+ */
52
+ export function mergeLocalOverrides(base, local) {
53
+ if (!local.reports || local.reports.length === 0)
54
+ return base;
55
+ const merged = base.reports.map((baseReport) => {
56
+ const localReport = local.reports.find((r) => r.id === baseReport.id);
57
+ if (!localReport)
58
+ return baseReport;
59
+ // Deep-merge: spread base then spread local — this replaces arrays as-is
60
+ return deepMergeReport(baseReport, localReport);
61
+ });
62
+ // Append reports from local that don't exist in base
63
+ for (const localReport of local.reports) {
64
+ const existsInBase = base.reports.some((r) => r.id === localReport.id);
65
+ if (!existsInBase) {
66
+ merged.push(localReport);
67
+ }
68
+ }
69
+ return { reports: merged };
70
+ }
71
+ /**
72
+ * Deep-merge two ReportDefinition objects. Local values override base values.
73
+ * Objects are recursively merged; arrays are replaced (not concatenated).
74
+ */
75
+ function deepMergeReport(base, local) {
76
+ const result = { ...base };
77
+ for (const [key, localVal] of Object.entries(local)) {
78
+ const baseVal = base[key];
79
+ if (localVal !== null &&
80
+ typeof localVal === 'object' &&
81
+ !Array.isArray(localVal) &&
82
+ baseVal !== null &&
83
+ typeof baseVal === 'object' &&
84
+ !Array.isArray(baseVal)) {
85
+ // Both are plain objects — recurse
86
+ result[key] = { ...baseVal, ...localVal };
87
+ }
88
+ else {
89
+ // Primitive, array, or null — local wins outright
90
+ result[key] = localVal;
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ /**
96
+ * Replace `{{var}}` placeholders in a template string with values from the
97
+ * vars map. Unknown placeholders are left as-is.
98
+ */
99
+ export function resolveTemplateVars(template, vars) {
100
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
101
+ return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match;
102
+ });
103
+ }
104
+ /**
105
+ * Derive standard template variables from environment:
106
+ * - `cw_base` — origin of CW_API_URL (e.g. https://na.myconnectwise.net)
107
+ * - `cw_version` — CW_WEB_VERSION env var (default: v2025_1)
108
+ * - `cw_locale` — CW_LOCALE env var (default: en_CA)
109
+ */
110
+ export function getTemplateVars() {
111
+ const apiUrl = process.env['CW_API_URL'] ?? '';
112
+ let cwBase = '';
113
+ if (apiUrl) {
114
+ try {
115
+ cwBase = new URL(apiUrl).origin;
116
+ }
117
+ catch {
118
+ // Malformed URL — leave empty
119
+ }
120
+ }
121
+ return {
122
+ cw_base: cwBase,
123
+ cw_version: process.env['CW_WEB_VERSION'] ?? 'v2025_1',
124
+ cw_locale: process.env['CW_LOCALE'] ?? 'en_CA',
125
+ };
126
+ }
127
+ //# sourceMappingURL=report-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-config.js","sourceRoot":"","sources":["../../src/services/report-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Decodes a base32-encoded string (RFC 4648) into a Buffer.
3
+ * Case-insensitive. Strips trailing '=' padding.
4
+ * Throws an Error with "invalid base32" for unrecognised characters.
5
+ */
6
+ export declare function decodeBase32(input: string): Buffer;
7
+ /**
8
+ * Generates a 6-digit TOTP code per RFC 6238 / HOTP RFC 4226.
9
+ *
10
+ * @param secret Base32-encoded shared secret
11
+ * @param counter Time step counter (defaults to current 30-second window)
12
+ */
13
+ export declare function generateTOTP(secret: string, counter?: number): string;
14
+ /**
15
+ * Returns the number of seconds remaining in the current 30-second TOTP window.
16
+ */
17
+ export declare function secondsRemaining(): number;
18
+ /**
19
+ * Generates a TOTP code, but waits for the next window first if fewer than
20
+ * `threshold` seconds (default 5) remain in the current window. This prevents
21
+ * codes from expiring mid-request.
22
+ *
23
+ * @param secret Base32-encoded shared secret
24
+ * @param threshold Minimum seconds remaining before generating (default 5)
25
+ */
26
+ export declare function generateTOTPSafe(secret: string, threshold?: number): Promise<string>;
27
+ //# sourceMappingURL=totp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp.d.ts","sourceRoot":"","sources":["../../src/services/totp.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAqBlD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAyBrE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,SAAS,SAAI,GACZ,OAAO,CAAC,MAAM,CAAC,CAOjB"}
@@ -0,0 +1,76 @@
1
+ import { createHmac } from 'node:crypto';
2
+ // RFC 4648 base32 alphabet
3
+ const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
4
+ /**
5
+ * Decodes a base32-encoded string (RFC 4648) into a Buffer.
6
+ * Case-insensitive. Strips trailing '=' padding.
7
+ * Throws an Error with "invalid base32" for unrecognised characters.
8
+ */
9
+ export function decodeBase32(input) {
10
+ const str = input.toUpperCase().replace(/=+$/, '');
11
+ let bits = 0;
12
+ let value = 0;
13
+ const bytes = [];
14
+ for (const char of str) {
15
+ const idx = BASE32_ALPHABET.indexOf(char);
16
+ if (idx === -1) {
17
+ throw new Error(`invalid base32 character: '${char}'`);
18
+ }
19
+ value = (value << 5) | idx;
20
+ bits += 5;
21
+ if (bits >= 8) {
22
+ bits -= 8;
23
+ bytes.push((value >>> bits) & 0xff);
24
+ }
25
+ }
26
+ return Buffer.from(bytes);
27
+ }
28
+ /**
29
+ * Generates a 6-digit TOTP code per RFC 6238 / HOTP RFC 4226.
30
+ *
31
+ * @param secret Base32-encoded shared secret
32
+ * @param counter Time step counter (defaults to current 30-second window)
33
+ */
34
+ export function generateTOTP(secret, counter) {
35
+ const step = counter ?? Math.floor(Date.now() / 1000 / 30);
36
+ const key = decodeBase32(secret);
37
+ // Encode counter as 8-byte big-endian buffer
38
+ const counterBuf = Buffer.alloc(8);
39
+ // JavaScript bitwise ops are 32-bit; handle upper 32 bits separately
40
+ const hi = Math.floor(step / 0x100000000);
41
+ const lo = step >>> 0;
42
+ counterBuf.writeUInt32BE(hi, 0);
43
+ counterBuf.writeUInt32BE(lo, 4);
44
+ const hmac = createHmac('sha1', key).update(counterBuf).digest();
45
+ // Dynamic truncation
46
+ const offset = hmac[hmac.length - 1] & 0x0f;
47
+ const code = (((hmac[offset] & 0x7f) << 24) |
48
+ ((hmac[offset + 1] & 0xff) << 16) |
49
+ ((hmac[offset + 2] & 0xff) << 8) |
50
+ (hmac[offset + 3] & 0xff)) %
51
+ 1_000_000;
52
+ return code.toString().padStart(6, '0');
53
+ }
54
+ /**
55
+ * Returns the number of seconds remaining in the current 30-second TOTP window.
56
+ */
57
+ export function secondsRemaining() {
58
+ return 30 - (Math.floor(Date.now() / 1000) % 30);
59
+ }
60
+ /**
61
+ * Generates a TOTP code, but waits for the next window first if fewer than
62
+ * `threshold` seconds (default 5) remain in the current window. This prevents
63
+ * codes from expiring mid-request.
64
+ *
65
+ * @param secret Base32-encoded shared secret
66
+ * @param threshold Minimum seconds remaining before generating (default 5)
67
+ */
68
+ export async function generateTOTPSafe(secret, threshold = 5) {
69
+ const remaining = secondsRemaining();
70
+ if (remaining < threshold) {
71
+ // Wait for the next window + 100 ms buffer
72
+ await new Promise((resolve) => setTimeout(resolve, remaining * 1000 + 100));
73
+ }
74
+ return generateTOTP(secret);
75
+ }
76
+ //# sourceMappingURL=totp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"totp.js","sourceRoot":"","sources":["../../src/services/totp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ import type { Page, Response as PWResponse } from 'playwright';
2
+ /**
3
+ * Returns true if the `playwright` package is installed and importable.
4
+ * Result is cached after the first call.
5
+ */
6
+ export declare function isPlaywrightAvailable(): Promise<boolean>;
7
+ /** Returns the singleton WebScraper instance (creates it on first call). */
8
+ export declare function getWebScraper(): WebScraper;
9
+ export declare class WebScraper {
10
+ private browser;
11
+ private context;
12
+ private initialized;
13
+ private semaphore;
14
+ private requestCount;
15
+ private contextCreatedAt;
16
+ private cleanupRegistered;
17
+ private authFailCount;
18
+ private readonly AUTH_FAIL_LIMIT;
19
+ constructor();
20
+ /**
21
+ * Launch the browser, restore storageState if available, and register
22
+ * process cleanup hooks. Safe to call multiple times — subsequent calls are
23
+ * no-ops if already initialized.
24
+ */
25
+ init(): Promise<void>;
26
+ /** Close the browser context and browser cleanly. */
27
+ shutdown(): Promise<void>;
28
+ /** Returns true if the browser has been launched and not yet shut down. */
29
+ isInitialized(): boolean;
30
+ /**
31
+ * Ensure the browser session is authenticated.
32
+ * Fast path: checks cookie expiry in storageState.
33
+ * Slow path: navigates to CW and runs the login flow.
34
+ */
35
+ ensureAuth(): Promise<void>;
36
+ /**
37
+ * Returns true if storageState contains at least one non-expired cookie
38
+ * (with more than SESSION_STALE_WINDOW_MS remaining).
39
+ */
40
+ private hasValidSession;
41
+ /**
42
+ * Returns true if the current page is showing a login form.
43
+ */
44
+ private isOnLoginPage;
45
+ /**
46
+ * Walk through the modular login steps, filling in credentials and TOTP as needed.
47
+ */
48
+ private authenticate;
49
+ /**
50
+ * Acquire an authenticated page.
51
+ * Recycles context if needed, ensures auth, waits for semaphore, creates page.
52
+ */
53
+ acquirePage(): Promise<Page>;
54
+ /**
55
+ * Release a page acquired via acquirePage().
56
+ * Closes the page, releases the semaphore, and cleans the downloads folder.
57
+ */
58
+ releasePage(page: Page): void;
59
+ /**
60
+ * Run `action` while listening for network responses matching `urlPattern`.
61
+ * Resolves with all matched responses once the pattern is satisfied or
62
+ * `timeoutMs` elapses.
63
+ */
64
+ captureResponses(page: Page, action: () => Promise<void>, urlPattern: string | RegExp, timeoutMs?: number): Promise<PWResponse[]>;
65
+ /** Close the existing context and create a fresh one, restoring storageState if present. */
66
+ private createContext;
67
+ /** Recycle the browser context if it has been used too much or too long. */
68
+ private recycleIfNeeded;
69
+ /** Register process-level cleanup hooks (once). */
70
+ private registerCleanupHooks;
71
+ /** Delete all files in the downloads directory. */
72
+ private cleanDownloads;
73
+ /** Derive the CW web base URL from CW_API_URL. */
74
+ private getCWBase;
75
+ }
76
+ //# sourceMappingURL=web-scraper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-scraper.d.ts","sourceRoot":"","sources":["../../src/services/web-scraper.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAA2B,IAAI,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,YAAY,CAAC;AA2ExF;;;GAGG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAS9D;AAQD,4EAA4E;AAC5E,wBAAgB,aAAa,IAAI,UAAU,CAG1C;AAMD,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,iBAAiB,CAAS;IAGlC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;;IAWrC;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B,qDAAqD;IAC/C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB/B,2EAA2E;IAC3E,aAAa,IAAI,OAAO;IAQxB;;;;OAIG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAqCjC;;;OAGG;IACH,OAAO,CAAC,eAAe;IAkBvB;;OAEG;YACW,aAAa;IAgB3B;;OAEG;YACW,YAAY;IAwD1B;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAclC;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAU7B;;;;OAIG;IACG,gBAAgB,CACpB,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC3B,UAAU,EAAE,MAAM,GAAG,MAAM,EAC3B,SAAS,SAAS,GACjB,OAAO,CAAC,UAAU,EAAE,CAAC;IAkCxB,4FAA4F;YAC9E,aAAa;IAsB3B,4EAA4E;YAC9D,eAAe;IAS7B,mDAAmD;IACnD,OAAO,CAAC,oBAAoB;IAkB5B,mDAAmD;IACnD,OAAO,CAAC,cAAc;IAgBtB,kDAAkD;IAClD,OAAO,CAAC,SAAS;CAYlB"}