@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.
@@ -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 };