devfortress-sdk 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +63 -0
- package/README.md +474 -0
- package/bin/devfortress-init.js +206 -0
- package/dist/browser.d.ts +61 -0
- package/dist/browser.js +184 -0
- package/dist/circuit-breaker.d.ts +68 -0
- package/dist/circuit-breaker.js +116 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +98 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +78 -0
- package/dist/middleware/express.d.ts +9 -0
- package/dist/middleware/express.js +236 -0
- package/dist/quick.d.ts +37 -0
- package/dist/quick.js +135 -0
- package/dist/types.d.ts +217 -0
- package/dist/types.js +12 -0
- package/package.json +101 -0
- package/src/middleware/fastapi.py +232 -0
- package/src/middleware/flask.py +213 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* DevFortress CLI Init Tool
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx devfortress-init
|
|
6
|
+
*
|
|
7
|
+
* Scaffolds DevFortress SDK integration into your project:
|
|
8
|
+
* 1. Detects framework (Express, Next.js, Fastify, Hono)
|
|
9
|
+
* 2. Creates devfortress.config.ts with zero-config defaults
|
|
10
|
+
* 3. Shows integration snippet for your framework
|
|
11
|
+
* 4. Target: under 3 minutes from install to first event
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const readline = require('readline');
|
|
17
|
+
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function ask(question) {
|
|
24
|
+
return new Promise(resolve => {
|
|
25
|
+
rl.question(question, resolve);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function detectFramework() {
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(
|
|
32
|
+
fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')
|
|
33
|
+
);
|
|
34
|
+
const deps = {
|
|
35
|
+
...pkg.dependencies,
|
|
36
|
+
...pkg.devDependencies,
|
|
37
|
+
};
|
|
38
|
+
if (deps.next) return 'nextjs';
|
|
39
|
+
if (deps.express) return 'express';
|
|
40
|
+
if (deps.fastify) return 'fastify';
|
|
41
|
+
if (deps.hono) return 'hono';
|
|
42
|
+
} catch {
|
|
43
|
+
/* no package.json */
|
|
44
|
+
}
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SNIPPETS = {
|
|
49
|
+
express: `// In your Express app entry point (e.g. app.ts or server.ts)
|
|
50
|
+
import { devFortressMiddleware } from 'devfortress-sdk';
|
|
51
|
+
|
|
52
|
+
// Add before your routes:
|
|
53
|
+
app.use(devFortressMiddleware({
|
|
54
|
+
apiKey: process.env.DEVFORTRESS_API_KEY!,
|
|
55
|
+
debug: process.env.NODE_ENV !== 'production',
|
|
56
|
+
}));`,
|
|
57
|
+
|
|
58
|
+
nextjs: `// In your Next.js middleware.ts (root of project)
|
|
59
|
+
import df from 'devfortress-sdk/quick';
|
|
60
|
+
|
|
61
|
+
const client = df.init({
|
|
62
|
+
apiKey: process.env.DEVFORTRESS_API_KEY!,
|
|
63
|
+
debug: process.env.NODE_ENV !== 'production',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// In your API routes:
|
|
67
|
+
export async function POST(req: NextRequest) {
|
|
68
|
+
await client.observe(req);
|
|
69
|
+
// ... your handler
|
|
70
|
+
}`,
|
|
71
|
+
|
|
72
|
+
fastify: `// In your Fastify server setup
|
|
73
|
+
import df from 'devfortress-sdk/quick';
|
|
74
|
+
|
|
75
|
+
const client = df.init({
|
|
76
|
+
apiKey: process.env.DEVFORTRESS_API_KEY!,
|
|
77
|
+
debug: process.env.NODE_ENV !== 'production',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
fastify.addHook('onRequest', async (request) => {
|
|
81
|
+
await client.observe(request.raw);
|
|
82
|
+
});`,
|
|
83
|
+
|
|
84
|
+
hono: `// In your Hono app
|
|
85
|
+
import df from 'devfortress-sdk/quick';
|
|
86
|
+
|
|
87
|
+
const client = df.init({
|
|
88
|
+
apiKey: process.env.DEVFORTRESS_API_KEY!,
|
|
89
|
+
debug: process.env.NODE_ENV !== 'production',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
app.use('*', async (c, next) => {
|
|
93
|
+
await client.observe(c.req.raw);
|
|
94
|
+
await next();
|
|
95
|
+
});`,
|
|
96
|
+
|
|
97
|
+
unknown: `// Generic integration
|
|
98
|
+
import df from 'devfortress-sdk/quick';
|
|
99
|
+
|
|
100
|
+
const client = df.init({
|
|
101
|
+
apiKey: process.env.DEVFORTRESS_API_KEY!,
|
|
102
|
+
debug: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Call observe() on every inbound request:
|
|
106
|
+
await client.observe(request);`,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log('🛡️ DevFortress SDK Setup');
|
|
112
|
+
console.log('─'.repeat(40));
|
|
113
|
+
console.log('');
|
|
114
|
+
|
|
115
|
+
const framework = detectFramework();
|
|
116
|
+
console.log(
|
|
117
|
+
`📦 Detected framework: ${framework === 'unknown' ? 'Could not detect' : framework}`
|
|
118
|
+
);
|
|
119
|
+
console.log('');
|
|
120
|
+
|
|
121
|
+
const apiKey = await ask(
|
|
122
|
+
'🔑 Enter your DevFortress API key (from dashboard): '
|
|
123
|
+
);
|
|
124
|
+
if (!apiKey || !apiKey.startsWith('df_')) {
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(
|
|
127
|
+
'⚠️ API key should start with "df_". Get one at https://devfortress.net/dashboard/settings/api-keys'
|
|
128
|
+
);
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log('Continuing with placeholder...');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const privacy = await ask('🔒 Privacy mode? (standard/strict) [standard]: ');
|
|
134
|
+
const privacyMode = privacy === 'strict' ? 'strict' : 'standard';
|
|
135
|
+
|
|
136
|
+
// Create config file
|
|
137
|
+
const configContent = `/**
|
|
138
|
+
* DevFortress Configuration
|
|
139
|
+
* Generated by devfortress-init on ${new Date().toISOString().split('T')[0]}
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
import df from 'devfortress-sdk/quick';
|
|
143
|
+
|
|
144
|
+
export const devfortress = df.init({
|
|
145
|
+
apiKey: process.env.DEVFORTRESS_API_KEY || '${apiKey || 'df_your_api_key'}',
|
|
146
|
+
privacy: '${privacyMode}',
|
|
147
|
+
debug: process.env.NODE_ENV !== 'production',
|
|
148
|
+
});
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const configPath = path.join(process.cwd(), 'devfortress.config.ts');
|
|
152
|
+
if (fs.existsSync(configPath)) {
|
|
153
|
+
const overwrite = await ask(
|
|
154
|
+
`⚠️ ${configPath} exists. Overwrite? (y/n) [n]: `
|
|
155
|
+
);
|
|
156
|
+
if (overwrite !== 'y') {
|
|
157
|
+
console.log('Skipping config file creation.');
|
|
158
|
+
} else {
|
|
159
|
+
fs.writeFileSync(configPath, configContent);
|
|
160
|
+
console.log(`✅ Created ${configPath}`);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
fs.writeFileSync(configPath, configContent);
|
|
164
|
+
console.log(`✅ Created ${configPath}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Add env variable
|
|
168
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
169
|
+
const envLine = `DEVFORTRESS_API_KEY=${apiKey || 'df_your_api_key'}`;
|
|
170
|
+
if (fs.existsSync(envPath)) {
|
|
171
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
172
|
+
if (!envContent.includes('DEVFORTRESS_API_KEY')) {
|
|
173
|
+
fs.appendFileSync(envPath, `\n${envLine}\n`);
|
|
174
|
+
console.log('✅ Added DEVFORTRESS_API_KEY to .env.local');
|
|
175
|
+
} else {
|
|
176
|
+
console.log('ℹ️ DEVFORTRESS_API_KEY already in .env.local');
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
fs.writeFileSync(envPath, `${envLine}\n`);
|
|
180
|
+
console.log('✅ Created .env.local with DEVFORTRESS_API_KEY');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Show integration snippet
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log('📝 Integration snippet for your framework:');
|
|
186
|
+
console.log('─'.repeat(40));
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(SNIPPETS[framework] || SNIPPETS.unknown);
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log('─'.repeat(40));
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log('🚀 Next steps:');
|
|
193
|
+
console.log(' 1. Add the integration snippet to your app');
|
|
194
|
+
console.log(' 2. Start your development server');
|
|
195
|
+
console.log(' 3. Make a request — see it in your DevFortress dashboard');
|
|
196
|
+
console.log('');
|
|
197
|
+
console.log('📖 Full docs: https://devfortress.net/docs/developer-guide');
|
|
198
|
+
console.log(
|
|
199
|
+
'🔒 Privacy info: https://devfortress.net/privacy/data-collected'
|
|
200
|
+
);
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
rl.close();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevFortress SDK - Browser Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Lightweight browser-compatible client for sending security events
|
|
5
|
+
* from client-side applications. Uses fetch API instead of axios.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export type { DevFortressClientOptions, LiveThreatEvent, EventType, SeverityLevel, ApiResponse, } from './types';
|
|
10
|
+
import type { DevFortressClientOptions } from './types';
|
|
11
|
+
/** @deprecated Use DevFortressClientOptions instead */
|
|
12
|
+
export type BrowserClientOptions = DevFortressClientOptions;
|
|
13
|
+
export declare class DevFortressBrowserClient {
|
|
14
|
+
private apiKey;
|
|
15
|
+
private endpoint;
|
|
16
|
+
private timeout;
|
|
17
|
+
private retries;
|
|
18
|
+
private debug;
|
|
19
|
+
constructor(options: DevFortressClientOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Track a security event from the browser
|
|
22
|
+
*/
|
|
23
|
+
trackEvent(event: {
|
|
24
|
+
eventType: string;
|
|
25
|
+
ip?: string;
|
|
26
|
+
method?: string;
|
|
27
|
+
path?: string;
|
|
28
|
+
userAgent?: string;
|
|
29
|
+
statusCode?: number;
|
|
30
|
+
responseTime?: number;
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
severity?: string;
|
|
33
|
+
reason?: string;
|
|
34
|
+
timestamp?: string;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
success: boolean;
|
|
37
|
+
eventId?: string;
|
|
38
|
+
message?: string;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Track a page error as a security event
|
|
42
|
+
*/
|
|
43
|
+
trackError(error: Error, context?: Record<string, unknown>): void;
|
|
44
|
+
/**
|
|
45
|
+
* Track a failed API response
|
|
46
|
+
*/
|
|
47
|
+
trackApiFailure(url: string, status: number, method?: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Install global error handler for automatic tracking
|
|
50
|
+
*/
|
|
51
|
+
installGlobalErrorHandler(): () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Test connection to the surveillance API
|
|
54
|
+
*/
|
|
55
|
+
testConnection(): Promise<boolean>;
|
|
56
|
+
private sendWithRetry;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a pre-configured browser client instance
|
|
60
|
+
*/
|
|
61
|
+
export declare function createBrowserClient(options: DevFortressClientOptions): DevFortressBrowserClient;
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DevFortress SDK - Browser Entry Point
|
|
4
|
+
*
|
|
5
|
+
* Lightweight browser-compatible client for sending security events
|
|
6
|
+
* from client-side applications. Uses fetch API instead of axios.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.DevFortressBrowserClient = void 0;
|
|
12
|
+
exports.createBrowserClient = createBrowserClient;
|
|
13
|
+
const DEFAULT_ENDPOINT = 'https://www.devfortress.net/api/events/ingest';
|
|
14
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
15
|
+
const DEFAULT_RETRIES = 3;
|
|
16
|
+
class DevFortressBrowserClient {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
if (!options.apiKey) {
|
|
19
|
+
throw new Error('DevFortressBrowserClient: apiKey is required');
|
|
20
|
+
}
|
|
21
|
+
this.apiKey = options.apiKey;
|
|
22
|
+
this.endpoint = options.endpoint || DEFAULT_ENDPOINT;
|
|
23
|
+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
24
|
+
this.retries = options.retries || DEFAULT_RETRIES;
|
|
25
|
+
this.debug = options.debug || false;
|
|
26
|
+
// Warn if endpoint is not HTTPS
|
|
27
|
+
if (this.endpoint.startsWith('http://') &&
|
|
28
|
+
!this.endpoint.includes('localhost') &&
|
|
29
|
+
!this.endpoint.includes('127.0.0.1')) {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.warn('[DevFortress] WARNING: Using non-HTTPS endpoint. API key will be transmitted in cleartext.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Track a security event from the browser
|
|
36
|
+
*/
|
|
37
|
+
async trackEvent(event) {
|
|
38
|
+
const payload = {
|
|
39
|
+
...event,
|
|
40
|
+
ip: event.ip || 'browser-client',
|
|
41
|
+
userAgent: event.userAgent ||
|
|
42
|
+
(typeof navigator !== 'undefined' ? navigator.userAgent : undefined),
|
|
43
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
return this.sendWithRetry(payload, 1);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Track a page error as a security event
|
|
49
|
+
*/
|
|
50
|
+
trackError(error, context) {
|
|
51
|
+
this.trackEvent({
|
|
52
|
+
eventType: 'custom',
|
|
53
|
+
reason: error.message,
|
|
54
|
+
metadata: {
|
|
55
|
+
stack: error.stack,
|
|
56
|
+
...context,
|
|
57
|
+
},
|
|
58
|
+
severity: 'MEDIUM',
|
|
59
|
+
}).catch(() => {
|
|
60
|
+
// Silently fail - don't crash the app for telemetry
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Track a failed API response
|
|
65
|
+
*/
|
|
66
|
+
trackApiFailure(url, status, method = 'GET') {
|
|
67
|
+
let eventType = 'custom';
|
|
68
|
+
let severity = 'LOW';
|
|
69
|
+
if (status === 401 || status === 403) {
|
|
70
|
+
eventType = 'auth_failure';
|
|
71
|
+
severity = 'MEDIUM';
|
|
72
|
+
}
|
|
73
|
+
else if (status === 429) {
|
|
74
|
+
eventType = 'rate_limit_exceeded';
|
|
75
|
+
severity = 'MEDIUM';
|
|
76
|
+
}
|
|
77
|
+
else if (status >= 500) {
|
|
78
|
+
eventType = '5xx_error';
|
|
79
|
+
severity = 'HIGH';
|
|
80
|
+
}
|
|
81
|
+
else if (status >= 400) {
|
|
82
|
+
eventType = '4xx_error';
|
|
83
|
+
severity = 'LOW';
|
|
84
|
+
}
|
|
85
|
+
this.trackEvent({
|
|
86
|
+
eventType,
|
|
87
|
+
method,
|
|
88
|
+
path: url,
|
|
89
|
+
statusCode: status,
|
|
90
|
+
severity,
|
|
91
|
+
reason: `API ${method} ${url} returned ${status}`,
|
|
92
|
+
}).catch(() => {
|
|
93
|
+
// Silently fail
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Install global error handler for automatic tracking
|
|
98
|
+
*/
|
|
99
|
+
installGlobalErrorHandler() {
|
|
100
|
+
if (typeof window === 'undefined')
|
|
101
|
+
return () => { };
|
|
102
|
+
const handler = (event) => {
|
|
103
|
+
this.trackError(event.error || new Error(event.message), {
|
|
104
|
+
filename: event.filename,
|
|
105
|
+
lineno: event.lineno,
|
|
106
|
+
colno: event.colno,
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const rejectionHandler = (event) => {
|
|
110
|
+
const error = event.reason instanceof Error
|
|
111
|
+
? event.reason
|
|
112
|
+
: new Error(String(event.reason));
|
|
113
|
+
this.trackError(error, { type: 'unhandledRejection' });
|
|
114
|
+
};
|
|
115
|
+
window.addEventListener('error', handler);
|
|
116
|
+
window.addEventListener('unhandledrejection', rejectionHandler);
|
|
117
|
+
// Return cleanup function
|
|
118
|
+
return () => {
|
|
119
|
+
window.removeEventListener('error', handler);
|
|
120
|
+
window.removeEventListener('unhandledrejection', rejectionHandler);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Test connection to the surveillance API
|
|
125
|
+
*/
|
|
126
|
+
async testConnection() {
|
|
127
|
+
try {
|
|
128
|
+
await this.trackEvent({
|
|
129
|
+
eventType: 'custom',
|
|
130
|
+
ip: 'browser-test',
|
|
131
|
+
metadata: { test: true },
|
|
132
|
+
});
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async sendWithRetry(payload, attempt) {
|
|
140
|
+
const controller = new AbortController();
|
|
141
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch(this.endpoint, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
'X-DevFortress-Key': this.apiKey,
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify(payload),
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
});
|
|
152
|
+
clearTimeout(timeoutId);
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
// Don't retry on client errors
|
|
155
|
+
if (response.status < 500) {
|
|
156
|
+
const errorData = await response.json().catch(() => ({}));
|
|
157
|
+
throw new Error(errorData.message || `HTTP ${response.status}`);
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Server error: ${response.status}`);
|
|
160
|
+
}
|
|
161
|
+
return await response.json();
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
clearTimeout(timeoutId);
|
|
165
|
+
if (attempt < this.retries) {
|
|
166
|
+
const backoff = Math.pow(2, attempt) * 500;
|
|
167
|
+
if (this.debug) {
|
|
168
|
+
// eslint-disable-next-line no-console
|
|
169
|
+
console.log(`[DevFortress] Retrying in ${backoff}ms (attempt ${attempt}/${this.retries})`);
|
|
170
|
+
}
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
172
|
+
return this.sendWithRetry(payload, attempt + 1);
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.DevFortressBrowserClient = DevFortressBrowserClient;
|
|
179
|
+
/**
|
|
180
|
+
* Create a pre-configured browser client instance
|
|
181
|
+
*/
|
|
182
|
+
function createBrowserClient(options) {
|
|
183
|
+
return new DevFortressBrowserClient(options);
|
|
184
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevFortress SDK — Platform Circuit Breaker
|
|
3
|
+
*
|
|
4
|
+
* Monitors connectivity to the DevFortress platform and provides
|
|
5
|
+
* automatic failover from external to internal closed-loop mode.
|
|
6
|
+
*
|
|
7
|
+
* Three states:
|
|
8
|
+
* - CLOSED: Platform is reachable, external CL is active
|
|
9
|
+
* - OPEN: Platform is unreachable, internal CL takes over
|
|
10
|
+
* - HALF-OPEN: Testing if platform has recovered
|
|
11
|
+
*
|
|
12
|
+
* Transition rules:
|
|
13
|
+
* CLOSED → OPEN: After `failureThreshold` consecutive failures
|
|
14
|
+
* OPEN → HALF-OPEN: After `recoveryTimeMs` elapsed
|
|
15
|
+
* HALF-OPEN → CLOSED: First successful call
|
|
16
|
+
* HALF-OPEN → OPEN: First failed call
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
export type CircuitState = 'closed' | 'open' | 'half-open';
|
|
21
|
+
export interface CircuitBreakerConfig {
|
|
22
|
+
/** Number of consecutive failures before opening the circuit. Default: 3 */
|
|
23
|
+
failureThreshold?: number;
|
|
24
|
+
/** Time in ms to wait before testing recovery. Default: 60_000 (1 min) */
|
|
25
|
+
recoveryTimeMs?: number;
|
|
26
|
+
/** Callback when circuit state changes */
|
|
27
|
+
onStateChange?: (from: CircuitState, to: CircuitState, reason: string) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare class PlatformCircuitBreaker {
|
|
30
|
+
private state;
|
|
31
|
+
private failureCount;
|
|
32
|
+
private lastFailureTime;
|
|
33
|
+
private failureThreshold;
|
|
34
|
+
private recoveryTimeMs;
|
|
35
|
+
private onStateChange?;
|
|
36
|
+
constructor(config?: CircuitBreakerConfig);
|
|
37
|
+
/**
|
|
38
|
+
* Check if external platform calls should be attempted.
|
|
39
|
+
* Returns true if the circuit allows external calls (CLOSED or HALF-OPEN).
|
|
40
|
+
*/
|
|
41
|
+
shouldCallExternal(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Record a successful external call.
|
|
44
|
+
*/
|
|
45
|
+
recordSuccess(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Record a failed external call.
|
|
48
|
+
*/
|
|
49
|
+
recordFailure(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Get current circuit state.
|
|
52
|
+
*/
|
|
53
|
+
getState(): CircuitState;
|
|
54
|
+
/**
|
|
55
|
+
* Get diagnostics info.
|
|
56
|
+
*/
|
|
57
|
+
getDiagnostics(): {
|
|
58
|
+
state: CircuitState;
|
|
59
|
+
failureCount: number;
|
|
60
|
+
lastFailureTime: number;
|
|
61
|
+
msUntilRecoveryAttempt: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Force reset the circuit breaker to closed state.
|
|
65
|
+
*/
|
|
66
|
+
reset(): void;
|
|
67
|
+
private transition;
|
|
68
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DevFortress SDK — Platform Circuit Breaker
|
|
4
|
+
*
|
|
5
|
+
* Monitors connectivity to the DevFortress platform and provides
|
|
6
|
+
* automatic failover from external to internal closed-loop mode.
|
|
7
|
+
*
|
|
8
|
+
* Three states:
|
|
9
|
+
* - CLOSED: Platform is reachable, external CL is active
|
|
10
|
+
* - OPEN: Platform is unreachable, internal CL takes over
|
|
11
|
+
* - HALF-OPEN: Testing if platform has recovered
|
|
12
|
+
*
|
|
13
|
+
* Transition rules:
|
|
14
|
+
* CLOSED → OPEN: After `failureThreshold` consecutive failures
|
|
15
|
+
* OPEN → HALF-OPEN: After `recoveryTimeMs` elapsed
|
|
16
|
+
* HALF-OPEN → CLOSED: First successful call
|
|
17
|
+
* HALF-OPEN → OPEN: First failed call
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.PlatformCircuitBreaker = void 0;
|
|
23
|
+
class PlatformCircuitBreaker {
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.state = 'closed';
|
|
26
|
+
this.failureCount = 0;
|
|
27
|
+
this.lastFailureTime = 0;
|
|
28
|
+
this.failureThreshold = config.failureThreshold ?? 3;
|
|
29
|
+
this.recoveryTimeMs = config.recoveryTimeMs ?? 60000;
|
|
30
|
+
this.onStateChange = config.onStateChange;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if external platform calls should be attempted.
|
|
34
|
+
* Returns true if the circuit allows external calls (CLOSED or HALF-OPEN).
|
|
35
|
+
*/
|
|
36
|
+
shouldCallExternal() {
|
|
37
|
+
if (this.state === 'closed')
|
|
38
|
+
return true;
|
|
39
|
+
if (this.state === 'open') {
|
|
40
|
+
// Check if recovery time has elapsed
|
|
41
|
+
if (Date.now() - this.lastFailureTime >= this.recoveryTimeMs) {
|
|
42
|
+
this.transition('half-open', 'recovery_timeout_elapsed');
|
|
43
|
+
return true; // Allow one test call
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// half-open — allow the test call
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Record a successful external call.
|
|
52
|
+
*/
|
|
53
|
+
recordSuccess() {
|
|
54
|
+
if (this.state === 'half-open') {
|
|
55
|
+
this.transition('closed', 'test_call_succeeded');
|
|
56
|
+
}
|
|
57
|
+
this.failureCount = 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Record a failed external call.
|
|
61
|
+
*/
|
|
62
|
+
recordFailure() {
|
|
63
|
+
this.failureCount++;
|
|
64
|
+
this.lastFailureTime = Date.now();
|
|
65
|
+
if (this.state === 'half-open') {
|
|
66
|
+
this.transition('open', 'test_call_failed');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (this.state === 'closed' && this.failureCount >= this.failureThreshold) {
|
|
70
|
+
this.transition('open', `${this.failureCount}_consecutive_failures`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get current circuit state.
|
|
75
|
+
*/
|
|
76
|
+
getState() {
|
|
77
|
+
return this.state;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get diagnostics info.
|
|
81
|
+
*/
|
|
82
|
+
getDiagnostics() {
|
|
83
|
+
const msUntilRecovery = this.state === 'open'
|
|
84
|
+
? Math.max(0, this.recoveryTimeMs - (Date.now() - this.lastFailureTime))
|
|
85
|
+
: 0;
|
|
86
|
+
return {
|
|
87
|
+
state: this.state,
|
|
88
|
+
failureCount: this.failureCount,
|
|
89
|
+
lastFailureTime: this.lastFailureTime,
|
|
90
|
+
msUntilRecoveryAttempt: msUntilRecovery,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Force reset the circuit breaker to closed state.
|
|
95
|
+
*/
|
|
96
|
+
reset() {
|
|
97
|
+
this.transition('closed', 'manual_reset');
|
|
98
|
+
this.failureCount = 0;
|
|
99
|
+
this.lastFailureTime = 0;
|
|
100
|
+
}
|
|
101
|
+
transition(to, reason) {
|
|
102
|
+
const from = this.state;
|
|
103
|
+
if (from === to)
|
|
104
|
+
return;
|
|
105
|
+
this.state = to;
|
|
106
|
+
if (this.onStateChange) {
|
|
107
|
+
try {
|
|
108
|
+
this.onStateChange(from, to, reason);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Callback errors should not break the circuit breaker
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.PlatformCircuitBreaker = PlatformCircuitBreaker;
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevFortress API Client
|
|
3
|
+
* Core client for sending events to DevFortress surveillance API
|
|
4
|
+
*/
|
|
5
|
+
import type { DevFortressClientOptions, LiveThreatEvent, ApiResponse } from './types';
|
|
6
|
+
export declare class DevFortressClient {
|
|
7
|
+
private apiKey;
|
|
8
|
+
private endpoint;
|
|
9
|
+
private timeout;
|
|
10
|
+
private retries;
|
|
11
|
+
private debug;
|
|
12
|
+
private axios;
|
|
13
|
+
constructor(options: DevFortressClientOptions);
|
|
14
|
+
/**
|
|
15
|
+
* Track a security event
|
|
16
|
+
*/
|
|
17
|
+
trackEvent(event: LiveThreatEvent): Promise<ApiResponse>;
|
|
18
|
+
/**
|
|
19
|
+
* Send event with automatic retry logic
|
|
20
|
+
*/
|
|
21
|
+
private sendWithRetry;
|
|
22
|
+
/**
|
|
23
|
+
* Test connection to surveillance API
|
|
24
|
+
*/
|
|
25
|
+
testConnection(): Promise<boolean>;
|
|
26
|
+
}
|