@nullbridge/sdk 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/README.md +232 -0
- package/package.json +25 -0
- package/src/agents.js +192 -0
- package/src/anomaly.js +148 -0
- package/src/audit.js +112 -0
- package/src/credentials.js +197 -0
- package/src/http.js +61 -0
- package/src/index.d.ts +136 -0
- package/src/index.js +152 -0
- package/src/license.js +100 -0
- package/src/siem.js +92 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CredentialVault — secure credential management for AI agents
|
|
7
|
+
* Stores, rotates, and revokes credentials with full audit trail
|
|
8
|
+
*/
|
|
9
|
+
class CredentialVault {
|
|
10
|
+
constructor(config, http) {
|
|
11
|
+
this._config = config;
|
|
12
|
+
this._http = http;
|
|
13
|
+
this._cache = new Map(); // local encrypted cache
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Store a credential for an agent
|
|
18
|
+
*
|
|
19
|
+
* @param {object} options
|
|
20
|
+
* @param {string} options.agentId - Agent ID this credential belongs to
|
|
21
|
+
* @param {string} options.name - Credential name (e.g. 'openai-api-key')
|
|
22
|
+
* @param {string} options.value - The secret value
|
|
23
|
+
* @param {string} [options.type] - Credential type: 'api_key' | 'oauth_token' | 'password' | 'certificate'
|
|
24
|
+
* @param {number} [options.ttl] - Time-to-live in seconds (0 = no expiry)
|
|
25
|
+
* @returns {Promise<string>} - Credential ID
|
|
26
|
+
*/
|
|
27
|
+
async store(options = {}) {
|
|
28
|
+
if (!options.agentId) throw new Error('[NullBridge] agentId is required');
|
|
29
|
+
if (!options.name) throw new Error('[NullBridge] credential name is required');
|
|
30
|
+
if (!options.value) throw new Error('[NullBridge] credential value is required');
|
|
31
|
+
|
|
32
|
+
const credId = this._generateCredId(options.agentId, options.name);
|
|
33
|
+
const encrypted = this._encrypt(options.value);
|
|
34
|
+
|
|
35
|
+
// Store locally (encrypted)
|
|
36
|
+
this._cache.set(credId, {
|
|
37
|
+
id: credId,
|
|
38
|
+
agentId: options.agentId,
|
|
39
|
+
name: options.name,
|
|
40
|
+
type: options.type || 'api_key',
|
|
41
|
+
value: encrypted,
|
|
42
|
+
ttl: options.ttl || 0,
|
|
43
|
+
storedAt: Math.floor(Date.now() / 1000),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Sync to NullBridge API (value is hashed, not stored in plaintext)
|
|
47
|
+
try {
|
|
48
|
+
await this._http.post(
|
|
49
|
+
this._config.apiUrl,
|
|
50
|
+
'/sdk/credentials/store',
|
|
51
|
+
{
|
|
52
|
+
license_key: this._config.licenseKey,
|
|
53
|
+
cred_id: credId,
|
|
54
|
+
agent_id: options.agentId,
|
|
55
|
+
name: options.name,
|
|
56
|
+
type: options.type || 'api_key',
|
|
57
|
+
value_hash: this._hash(options.value),
|
|
58
|
+
ttl: options.ttl || 0,
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
} catch {
|
|
62
|
+
// Continue with local storage if API unreachable
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return credId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve a credential value
|
|
70
|
+
*
|
|
71
|
+
* @param {string} credId - Credential ID returned from store()
|
|
72
|
+
* @returns {Promise<string|null>} - Decrypted credential value
|
|
73
|
+
*/
|
|
74
|
+
async get(credId) {
|
|
75
|
+
const cached = this._cache.get(credId);
|
|
76
|
+
if (!cached) return null;
|
|
77
|
+
|
|
78
|
+
// Check TTL
|
|
79
|
+
if (cached.ttl > 0) {
|
|
80
|
+
const age = Math.floor(Date.now() / 1000) - cached.storedAt;
|
|
81
|
+
if (age > cached.ttl) {
|
|
82
|
+
this._cache.delete(credId);
|
|
83
|
+
console.warn(`[NullBridge] Credential ${credId} has expired (TTL: ${cached.ttl}s)`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this._decrypt(cached.value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Rotate a credential — replaces the value and logs the rotation
|
|
93
|
+
*
|
|
94
|
+
* @param {string} credId - Credential ID to rotate
|
|
95
|
+
* @param {string} newValue - New credential value
|
|
96
|
+
*/
|
|
97
|
+
async rotate(credId, newValue) {
|
|
98
|
+
if (!newValue) throw new Error('[NullBridge] newValue is required for rotation');
|
|
99
|
+
|
|
100
|
+
const cached = this._cache.get(credId);
|
|
101
|
+
if (!cached) throw new Error(`[NullBridge] Credential ${credId} not found`);
|
|
102
|
+
|
|
103
|
+
const encrypted = this._encrypt(newValue);
|
|
104
|
+
this._cache.set(credId, { ...cached, value: encrypted, storedAt: Math.floor(Date.now() / 1000) });
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await this._http.post(
|
|
108
|
+
this._config.apiUrl,
|
|
109
|
+
'/sdk/credentials/rotate',
|
|
110
|
+
{
|
|
111
|
+
license_key: this._config.licenseKey,
|
|
112
|
+
cred_id: credId,
|
|
113
|
+
value_hash: this._hash(newValue),
|
|
114
|
+
rotated_at: Math.floor(Date.now() / 1000),
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
console.info(`[NullBridge] Credential rotated: ${cached.name} (${credId})`);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.warn(`[NullBridge] Credential rotated locally only (API unreachable): ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Revoke a credential — deletes it and logs revocation
|
|
125
|
+
*
|
|
126
|
+
* @param {string} credId - Credential ID to revoke
|
|
127
|
+
*/
|
|
128
|
+
async revoke(credId) {
|
|
129
|
+
const cached = this._cache.get(credId);
|
|
130
|
+
this._cache.delete(credId);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await this._http.post(
|
|
134
|
+
this._config.apiUrl,
|
|
135
|
+
'/sdk/credentials/revoke',
|
|
136
|
+
{
|
|
137
|
+
license_key: this._config.licenseKey,
|
|
138
|
+
cred_id: credId,
|
|
139
|
+
revoked_at: Math.floor(Date.now() / 1000),
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
console.info(`[NullBridge] Credential revoked: ${cached?.name || credId}`);
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* List all credentials for an agent (metadata only, no values)
|
|
148
|
+
*/
|
|
149
|
+
list(agentId) {
|
|
150
|
+
const results = [];
|
|
151
|
+
for (const [id, cred] of this._cache.entries()) {
|
|
152
|
+
if (cred.agentId === agentId) {
|
|
153
|
+
results.push({ id, name: cred.name, type: cred.type, storedAt: cred.storedAt, ttl: cred.ttl });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Encryption helpers (AES-256-GCM) ─────────────────────────────────────
|
|
160
|
+
_getKey() {
|
|
161
|
+
// Derive encryption key from license key
|
|
162
|
+
return crypto.createHash('sha256').update(this._config.licenseKey).digest();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_encrypt(plaintext) {
|
|
166
|
+
const key = this._getKey();
|
|
167
|
+
const iv = crypto.randomBytes(12);
|
|
168
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
169
|
+
const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
170
|
+
const tag = cipher.getAuthTag();
|
|
171
|
+
return Buffer.concat([iv, tag, enc]).toString('base64');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_decrypt(ciphertext) {
|
|
175
|
+
const key = this._getKey();
|
|
176
|
+
const buf = Buffer.from(ciphertext, 'base64');
|
|
177
|
+
const iv = buf.slice(0, 12);
|
|
178
|
+
const tag = buf.slice(12, 28);
|
|
179
|
+
const enc = buf.slice(28);
|
|
180
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
181
|
+
decipher.setAuthTag(tag);
|
|
182
|
+
return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_hash(value) {
|
|
186
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_generateCredId(agentId, name) {
|
|
190
|
+
return 'cred-' + crypto.createHash('sha256')
|
|
191
|
+
.update(agentId + name + Date.now())
|
|
192
|
+
.digest('hex')
|
|
193
|
+
.slice(0, 16);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { CredentialVault };
|
package/src/http.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
|
|
6
|
+
class HttpClient {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this._config = config;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
post(baseUrl, path, body, timeoutMs = 10000) {
|
|
12
|
+
return this._request('POST', baseUrl, path, body, timeoutMs);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get(baseUrl, path, timeoutMs = 10000) {
|
|
16
|
+
return this._request('GET', baseUrl, path, null, timeoutMs);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_request(method, baseUrl, path, body, timeoutMs) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const parsed = new URL(baseUrl + path);
|
|
22
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
23
|
+
const data = body ? JSON.stringify(body) : null;
|
|
24
|
+
|
|
25
|
+
const options = {
|
|
26
|
+
hostname: parsed.hostname,
|
|
27
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
28
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
29
|
+
method,
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'User-Agent': `nullbridge-sdk/1.0.0 node/${process.version}`,
|
|
33
|
+
'X-SDK-Version': '1.0.0',
|
|
34
|
+
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
|
|
35
|
+
},
|
|
36
|
+
timeout: timeoutMs,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const req = lib.request(options, res => {
|
|
40
|
+
let raw = '';
|
|
41
|
+
res.on('data', chunk => raw += chunk);
|
|
42
|
+
res.on('end', () => {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
resolve({ status: res.statusCode, body: parsed });
|
|
46
|
+
} catch {
|
|
47
|
+
resolve({ status: res.statusCode, body: { raw } });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
req.on('error', reject);
|
|
53
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
54
|
+
|
|
55
|
+
if (data) req.write(data);
|
|
56
|
+
req.end();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { HttpClient };
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export interface NullBridgeConfig {
|
|
2
|
+
licenseKey: string;
|
|
3
|
+
serverUrl?: string;
|
|
4
|
+
apiUrl?: string;
|
|
5
|
+
skipLicense?: boolean;
|
|
6
|
+
autoShutdown?: boolean;
|
|
7
|
+
checkInterval?: number;
|
|
8
|
+
siem?: {
|
|
9
|
+
endpoint: string;
|
|
10
|
+
format?: 'json' | 'cef' | 'leef';
|
|
11
|
+
};
|
|
12
|
+
debug?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LicenseData {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
reason: string;
|
|
18
|
+
customer: string;
|
|
19
|
+
company: string;
|
|
20
|
+
plan: 'starter' | 'professional' | 'enterprise';
|
|
21
|
+
max_agents: number;
|
|
22
|
+
expires_at: number | null;
|
|
23
|
+
warning?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AgentOptions {
|
|
27
|
+
name: string;
|
|
28
|
+
type: 'llm' | 'rpa' | 'workflow' | 'custom';
|
|
29
|
+
model?: string;
|
|
30
|
+
scopes?: string[];
|
|
31
|
+
metadata?: Record<string, any>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AuditEvent {
|
|
35
|
+
agentId: string;
|
|
36
|
+
agentName?: string;
|
|
37
|
+
action: string;
|
|
38
|
+
resource?: string;
|
|
39
|
+
outcome?: 'success' | 'failure' | 'blocked';
|
|
40
|
+
details?: Record<string, any>;
|
|
41
|
+
ip?: string;
|
|
42
|
+
duration?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AnomalyAlert {
|
|
46
|
+
agentId: string;
|
|
47
|
+
metric: string;
|
|
48
|
+
value: number;
|
|
49
|
+
mean: number;
|
|
50
|
+
zScore: number;
|
|
51
|
+
severity: 'medium' | 'high' | 'critical';
|
|
52
|
+
timestamp: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CredentialOptions {
|
|
56
|
+
agentId: string;
|
|
57
|
+
name: string;
|
|
58
|
+
value: string;
|
|
59
|
+
type?: 'api_key' | 'oauth_token' | 'password' | 'certificate';
|
|
60
|
+
ttl?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export declare class Agent {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
type: string;
|
|
67
|
+
model: string | null;
|
|
68
|
+
scopes: string[];
|
|
69
|
+
metadata: Record<string, any>;
|
|
70
|
+
logAction(action: string, details?: Record<string, any>): Promise<void>;
|
|
71
|
+
hasScope(scope: string): boolean;
|
|
72
|
+
update(updates: Record<string, any>): Promise<void>;
|
|
73
|
+
deregister(): Promise<void>;
|
|
74
|
+
toJSON(): object;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export declare class AgentRegistry {
|
|
78
|
+
register(options: AgentOptions): Promise<Agent>;
|
|
79
|
+
list(): Promise<Agent[]>;
|
|
80
|
+
get(agentId: string): Agent | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export declare class CredentialVault {
|
|
84
|
+
store(options: CredentialOptions): Promise<string>;
|
|
85
|
+
get(credId: string): Promise<string | null>;
|
|
86
|
+
rotate(credId: string, newValue: string): Promise<void>;
|
|
87
|
+
revoke(credId: string): Promise<void>;
|
|
88
|
+
list(agentId: string): object[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export declare class AuditLogger {
|
|
92
|
+
log(event: AuditEvent): string;
|
|
93
|
+
logViolation(agentId: string, agentName: string, action: string, reason: string, details?: object): string;
|
|
94
|
+
flush(): Promise<void>;
|
|
95
|
+
getQueue(): object[];
|
|
96
|
+
stop(): void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export declare class AnomalyDetector {
|
|
100
|
+
record(agentId: string, metric: string, value: number): void;
|
|
101
|
+
check(agentId: string, metric: string, value: number): { anomalous: boolean; zScore: number; mean: number };
|
|
102
|
+
onAlert(handler: (alert: AnomalyAlert) => void): AnomalyDetector;
|
|
103
|
+
getBaseline(agentId: string): object | null;
|
|
104
|
+
resetBaseline(agentId: string, metric?: string): void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export declare class NullBridge {
|
|
108
|
+
license: LicenseClient;
|
|
109
|
+
agents: AgentRegistry;
|
|
110
|
+
credentials: CredentialVault;
|
|
111
|
+
audit: AuditLogger;
|
|
112
|
+
anomaly: AnomalyDetector;
|
|
113
|
+
siem: SiemStreamer | null;
|
|
114
|
+
|
|
115
|
+
constructor(config: NullBridgeConfig);
|
|
116
|
+
init(): Promise<NullBridge>;
|
|
117
|
+
shutdown(): Promise<void>;
|
|
118
|
+
getLicenseInfo(): LicenseData | null;
|
|
119
|
+
getVersion(): string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export declare class LicenseClient {
|
|
123
|
+
validate(): Promise<LicenseData | null>;
|
|
124
|
+
startWatcher(onInvalid: (valid: boolean, reason: string) => void): void;
|
|
125
|
+
stopWatcher(): void;
|
|
126
|
+
getData(): LicenseData | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export declare class SiemStreamer {
|
|
130
|
+
start(): Promise<void>;
|
|
131
|
+
push(event: object): void;
|
|
132
|
+
flush(): Promise<void>;
|
|
133
|
+
stop(): void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { NullBridge as default };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { LicenseClient } = require('./license');
|
|
4
|
+
const { AgentRegistry } = require('./agents');
|
|
5
|
+
const { CredentialVault } = require('./credentials');
|
|
6
|
+
const { AuditLogger } = require('./audit');
|
|
7
|
+
const { AnomalyDetector } = require('./anomaly');
|
|
8
|
+
const { SiemStreamer } = require('./siem');
|
|
9
|
+
const { HttpClient } = require('./http');
|
|
10
|
+
|
|
11
|
+
const SDK_VERSION = '1.0.0';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* NullBridge SDK
|
|
15
|
+
* AI Agent Identity Governance Platform
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const { NullBridge } = require('@nullbridge/sdk');
|
|
19
|
+
*
|
|
20
|
+
* const nb = new NullBridge({
|
|
21
|
+
* licenseKey: process.env.NULLBRIDGE_LICENSE_KEY,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* await nb.init();
|
|
25
|
+
* const agent = await nb.agents.register({ name: 'my-agent', type: 'llm' });
|
|
26
|
+
*/
|
|
27
|
+
class NullBridge {
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} config
|
|
30
|
+
* @param {string} config.licenseKey - Your NullBridge license key (NB-XXXX-XXXX-XXXX-XXXX)
|
|
31
|
+
* @param {string} [config.serverUrl] - License server URL (default: https://nullbridge-license-server-production.up.railway.app)
|
|
32
|
+
* @param {string} [config.apiUrl] - NullBridge API URL (default: https://api.nullbridge.ai)
|
|
33
|
+
* @param {boolean} [config.skipLicense] - Skip license check (for development only)
|
|
34
|
+
* @param {boolean} [config.autoShutdown] - Shut down process if license is revoked (default: true)
|
|
35
|
+
* @param {number} [config.checkInterval] - License check interval in ms (default: 86400000 — 24 hours)
|
|
36
|
+
* @param {object} [config.siem] - SIEM streaming config
|
|
37
|
+
* @param {string} [config.siem.endpoint] - SIEM webhook URL
|
|
38
|
+
* @param {string} [config.siem.format] - Log format: 'json' | 'cef' | 'leef' (default: 'json')
|
|
39
|
+
* @param {boolean} [config.debug] - Enable debug logging
|
|
40
|
+
*/
|
|
41
|
+
constructor(config = {}) {
|
|
42
|
+
if (!config.licenseKey && !config.skipLicense) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'[NullBridge] licenseKey is required. Set NULLBRIDGE_LICENSE_KEY in your environment variables.\n' +
|
|
45
|
+
'Contact brian@nullbridge.ai to obtain a license key.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this._config = {
|
|
50
|
+
licenseKey: config.licenseKey || '',
|
|
51
|
+
serverUrl: config.serverUrl || 'https://nullbridge-license-server-production.up.railway.app',
|
|
52
|
+
apiUrl: config.apiUrl || 'https://api.nullbridge.ai',
|
|
53
|
+
skipLicense: config.skipLicense || false,
|
|
54
|
+
autoShutdown: config.autoShutdown !== false,
|
|
55
|
+
checkInterval: config.checkInterval || 24 * 60 * 60 * 1000,
|
|
56
|
+
siem: config.siem || null,
|
|
57
|
+
debug: config.debug || false,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this._initialized = false;
|
|
61
|
+
this._licenseData = null;
|
|
62
|
+
this._http = new HttpClient(this._config);
|
|
63
|
+
|
|
64
|
+
// Sub-modules — available after init()
|
|
65
|
+
this.license = new LicenseClient(this._config, this._http);
|
|
66
|
+
this.agents = new AgentRegistry(this._config, this._http);
|
|
67
|
+
this.credentials = new CredentialVault(this._config, this._http);
|
|
68
|
+
this.audit = new AuditLogger(this._config, this._http);
|
|
69
|
+
this.anomaly = new AnomalyDetector(this._config, this._http);
|
|
70
|
+
this.siem = this._config.siem ? new SiemStreamer(this._config, this._http) : null;
|
|
71
|
+
|
|
72
|
+
this._log('NullBridge SDK v' + SDK_VERSION + ' initialized');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Initialize NullBridge — validates license and starts background services.
|
|
77
|
+
* Call this once on application startup, before serving traffic.
|
|
78
|
+
*
|
|
79
|
+
* @returns {Promise<NullBridge>} — returns this for chaining
|
|
80
|
+
* @throws {Error} if license is invalid or expired
|
|
81
|
+
*/
|
|
82
|
+
async init() {
|
|
83
|
+
if (this._initialized) {
|
|
84
|
+
this._log('Already initialized — skipping');
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._log('Initializing...');
|
|
89
|
+
|
|
90
|
+
// Validate license
|
|
91
|
+
if (!this._config.skipLicense) {
|
|
92
|
+
this._licenseData = await this.license.validate();
|
|
93
|
+
this._log(`License valid — customer: ${this._licenseData.customer}, plan: ${this._licenseData.plan}`);
|
|
94
|
+
|
|
95
|
+
// Start periodic license checks
|
|
96
|
+
this.license.startWatcher((valid, reason) => {
|
|
97
|
+
if (!valid) {
|
|
98
|
+
console.error(`[NullBridge] License revoked: ${reason}`);
|
|
99
|
+
if (this._config.autoShutdown) {
|
|
100
|
+
console.error('[NullBridge] Shutting down in 60 seconds. Contact brian@nullbridge.ai.');
|
|
101
|
+
setTimeout(() => process.exit(1), 60 * 1000);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
this._log('License check skipped (development mode)');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Start SIEM streaming if configured
|
|
110
|
+
if (this.siem) {
|
|
111
|
+
await this.siem.start();
|
|
112
|
+
this._log('SIEM streaming started');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this._initialized = true;
|
|
116
|
+
console.info('[NullBridge] Ready');
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Gracefully shut down NullBridge — flushes audit logs and stops watchers.
|
|
122
|
+
*/
|
|
123
|
+
async shutdown() {
|
|
124
|
+
this._log('Shutting down...');
|
|
125
|
+
this.license.stopWatcher();
|
|
126
|
+
if (this.siem) await this.siem.flush();
|
|
127
|
+
this._initialized = false;
|
|
128
|
+
this._log('Shutdown complete');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns current license data (customer, plan, max_agents, expires_at)
|
|
133
|
+
*/
|
|
134
|
+
getLicenseInfo() {
|
|
135
|
+
return this._licenseData;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Returns SDK version
|
|
140
|
+
*/
|
|
141
|
+
getVersion() {
|
|
142
|
+
return SDK_VERSION;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_log(msg) {
|
|
146
|
+
if (this._config.debug) {
|
|
147
|
+
console.debug(`[NullBridge] ${msg}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { NullBridge, SDK_VERSION };
|
package/src/license.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class LicenseClient {
|
|
4
|
+
constructor(config, http) {
|
|
5
|
+
this._config = config;
|
|
6
|
+
this._http = http;
|
|
7
|
+
this._watcher = null;
|
|
8
|
+
this._data = null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate license against NullBridge license server.
|
|
13
|
+
* Throws if license is invalid, expired, suspended, or cancelled.
|
|
14
|
+
*/
|
|
15
|
+
async validate() {
|
|
16
|
+
try {
|
|
17
|
+
const { status, body } = await this._http.post(
|
|
18
|
+
this._config.serverUrl,
|
|
19
|
+
'/api/validate',
|
|
20
|
+
{ key: this._config.licenseKey }
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (status === 200 && body.valid) {
|
|
24
|
+
this._data = body;
|
|
25
|
+
return body;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// License exists but is not valid
|
|
29
|
+
const reason = body.reason || 'UNKNOWN';
|
|
30
|
+
const msg = body.message || 'License validation failed.';
|
|
31
|
+
|
|
32
|
+
const errors = {
|
|
33
|
+
SUSPENDED: `License suspended. Contact brian@nullbridge.ai to restore service.`,
|
|
34
|
+
CANCELLED: `License cancelled. Contact brian@nullbridge.ai.`,
|
|
35
|
+
EXPIRED: `License expired. Contact brian@nullbridge.ai to renew.`,
|
|
36
|
+
NOT_FOUND: `License key not found. Verify your NULLBRIDGE_LICENSE_KEY is correct.`,
|
|
37
|
+
GRACE_PERIOD: null, // still valid — handled below
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (reason === 'GRACE_PERIOD') {
|
|
41
|
+
console.warn(`[NullBridge] Warning: ${msg}`);
|
|
42
|
+
this._data = body;
|
|
43
|
+
return body;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error(`[NullBridge] ${errors[reason] || msg}`);
|
|
47
|
+
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.message.startsWith('[NullBridge]')) throw err;
|
|
50
|
+
|
|
51
|
+
// Network error — allow startup but warn
|
|
52
|
+
console.warn(`[NullBridge] License server unreachable (${err.message}). Allowing startup — will retry in 24 hours.`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start periodic license checks.
|
|
59
|
+
* @param {function} onInvalid - callback(valid, reason) when license becomes invalid
|
|
60
|
+
*/
|
|
61
|
+
startWatcher(onInvalid) {
|
|
62
|
+
if (this._watcher) return;
|
|
63
|
+
|
|
64
|
+
this._watcher = setInterval(async () => {
|
|
65
|
+
try {
|
|
66
|
+
const { status, body } = await this._http.post(
|
|
67
|
+
this._config.serverUrl,
|
|
68
|
+
'/api/validate',
|
|
69
|
+
{ key: this._config.licenseKey }
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (status !== 200 || !body.valid) {
|
|
73
|
+
if (body.reason !== 'GRACE_PERIOD') {
|
|
74
|
+
onInvalid(false, body.reason || 'UNKNOWN');
|
|
75
|
+
} else {
|
|
76
|
+
console.warn(`[NullBridge] License check: grace period — payment overdue.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn(`[NullBridge] License check: server unreachable. Will retry in 24 hours.`);
|
|
81
|
+
}
|
|
82
|
+
}, this._config.checkInterval);
|
|
83
|
+
|
|
84
|
+
// Don't let the watcher keep the process alive
|
|
85
|
+
if (this._watcher.unref) this._watcher.unref();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
stopWatcher() {
|
|
89
|
+
if (this._watcher) {
|
|
90
|
+
clearInterval(this._watcher);
|
|
91
|
+
this._watcher = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getData() {
|
|
96
|
+
return this._data;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { LicenseClient };
|