cloudcruise 1.0.0 → 1.1.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/dist/CloudCruise.d.ts +29 -0
- package/dist/CloudCruise.js +113 -0
- package/dist/events/types.d.ts +186 -0
- package/dist/events/types.js +23 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +13 -0
- package/dist/runs/RunsClient.d.ts +101 -0
- package/dist/runs/RunsClient.js +400 -0
- package/dist/runs/types.d.ts +157 -0
- package/dist/runs/types.js +4 -0
- package/dist/utils/asyncQueue.d.ts +9 -0
- package/dist/utils/asyncQueue.js +43 -0
- package/dist/utils/connectionManager.d.ts +29 -0
- package/dist/utils/connectionManager.js +234 -0
- package/dist/utils/env.d.ts +2 -0
- package/dist/utils/env.js +9 -0
- package/dist/utils/events.d.ts +24 -0
- package/dist/utils/events.js +40 -0
- package/dist/utils/sse.d.ts +24 -0
- package/dist/utils/sse.js +122 -0
- package/dist/vault/VaultClient.d.ts +55 -0
- package/dist/vault/VaultClient.js +115 -0
- package/dist/vault/types.d.ts +62 -0
- package/dist/vault/types.js +4 -0
- package/dist/vault/utils.d.ts +34 -0
- package/dist/vault/utils.js +122 -0
- package/dist/webhook/WebhookClient.d.ts +15 -0
- package/dist/webhook/WebhookClient.js +18 -0
- package/dist/webhook/types.d.ts +7 -0
- package/dist/webhook/types.js +8 -0
- package/dist/webhook/utils.d.ts +3 -0
- package/dist/webhook/utils.js +49 -0
- package/dist/workflows/WorkflowsClient.d.ts +19 -0
- package/dist/workflows/WorkflowsClient.js +97 -0
- package/dist/workflows/types.d.ts +41 -0
- package/dist/workflows/types.js +15 -0
- package/package.json +2 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { openSSE } from './sse.js';
|
|
2
|
+
import { SimpleEventEmitter } from './events.js';
|
|
3
|
+
import { AsyncEventQueue } from './asyncQueue.js';
|
|
4
|
+
import { EventType } from '../events/types.js';
|
|
5
|
+
function isFinalEvent(eventType) {
|
|
6
|
+
return (eventType === EventType.ExecutionSuccess ||
|
|
7
|
+
eventType === EventType.ExecutionFailed ||
|
|
8
|
+
eventType === EventType.ExecutionStopped);
|
|
9
|
+
}
|
|
10
|
+
export class ConnectionManager {
|
|
11
|
+
baseUrl;
|
|
12
|
+
apiKey;
|
|
13
|
+
clientId;
|
|
14
|
+
conn = null;
|
|
15
|
+
connecting = false;
|
|
16
|
+
connected = false;
|
|
17
|
+
reconnecting = false;
|
|
18
|
+
reconnectDelays = [1000, 3000, 10000];
|
|
19
|
+
sessions = new Map();
|
|
20
|
+
constructor(baseUrl, apiKey) {
|
|
21
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
22
|
+
this.apiKey = apiKey;
|
|
23
|
+
}
|
|
24
|
+
async ensureClientId() {
|
|
25
|
+
if (this.clientId)
|
|
26
|
+
return this.clientId;
|
|
27
|
+
this.clientId = this.generateClientId();
|
|
28
|
+
return this.clientId;
|
|
29
|
+
}
|
|
30
|
+
/*
|
|
31
|
+
try to use crypto.randomUUID if the platform is supported. otherwise fallback to other methods: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid/2117523#2117523
|
|
32
|
+
*/
|
|
33
|
+
generateClientId() {
|
|
34
|
+
const cryptoObj = globalThis.crypto;
|
|
35
|
+
// Preferred: native CSPRNG-backed UUID
|
|
36
|
+
if (cryptoObj?.randomUUID) {
|
|
37
|
+
return cryptoObj.randomUUID();
|
|
38
|
+
}
|
|
39
|
+
// Fallback: RFC4122 v4 built from CSPRNG bytes
|
|
40
|
+
if (cryptoObj?.getRandomValues) {
|
|
41
|
+
const bytes = new Uint8Array(16);
|
|
42
|
+
cryptoObj.getRandomValues(bytes);
|
|
43
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
|
44
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10xx
|
|
45
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));
|
|
46
|
+
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`;
|
|
47
|
+
}
|
|
48
|
+
// Last resort: non-CSPRNG timestamp + Math.random
|
|
49
|
+
let d = Date.now();
|
|
50
|
+
let d2 = typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
51
|
+
? Math.floor(performance.now() * 1000)
|
|
52
|
+
: 0;
|
|
53
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
54
|
+
let r = Math.random() * 16;
|
|
55
|
+
if (d > 0) {
|
|
56
|
+
r = ((d + r) % 16) | 0;
|
|
57
|
+
d = Math.floor(d / 16);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
r = ((d2 + r) % 16) | 0;
|
|
61
|
+
d2 = Math.floor(d2 / 16);
|
|
62
|
+
}
|
|
63
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
64
|
+
return v.toString(16);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async connectIfNeeded() {
|
|
68
|
+
if (this.connected || this.connecting)
|
|
69
|
+
return;
|
|
70
|
+
if (!this.clientId)
|
|
71
|
+
await this.ensureClientId();
|
|
72
|
+
await this.openMuxConnection();
|
|
73
|
+
}
|
|
74
|
+
subscribe(sessionId, opts) {
|
|
75
|
+
// Kick off connection if not already connected
|
|
76
|
+
try {
|
|
77
|
+
void this.connectIfNeeded();
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
// Ensure channel exists
|
|
81
|
+
let channel = this.sessions.get(sessionId);
|
|
82
|
+
if (!channel) {
|
|
83
|
+
channel = {
|
|
84
|
+
sessionId,
|
|
85
|
+
emitter: new SimpleEventEmitter(),
|
|
86
|
+
subscribers: new Set(),
|
|
87
|
+
ended: false
|
|
88
|
+
};
|
|
89
|
+
this.sessions.set(sessionId, channel);
|
|
90
|
+
}
|
|
91
|
+
// Create per-handle queue
|
|
92
|
+
const queue = new AsyncEventQueue();
|
|
93
|
+
channel.subscribers.add(queue);
|
|
94
|
+
// Propagate abort to handle close
|
|
95
|
+
if (opts?.signal) {
|
|
96
|
+
const onAbort = () => sub.close();
|
|
97
|
+
opts.signal.addEventListener('abort', onAbort, { once: true });
|
|
98
|
+
}
|
|
99
|
+
const sub = {
|
|
100
|
+
on: (event, handler) => channel.emitter.on(event, handler),
|
|
101
|
+
close: () => {
|
|
102
|
+
if (!channel)
|
|
103
|
+
return;
|
|
104
|
+
queue.close();
|
|
105
|
+
channel.subscribers.delete(queue);
|
|
106
|
+
// If no more subscribers and channel is ended, remove it
|
|
107
|
+
if (channel.subscribers.size === 0 && channel.ended) {
|
|
108
|
+
this.sessions.delete(sessionId);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
[Symbol.asyncIterator]() {
|
|
112
|
+
return queue[Symbol.asyncIterator]();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
return sub;
|
|
116
|
+
}
|
|
117
|
+
async openMuxConnection() {
|
|
118
|
+
if (this.connecting || this.connected)
|
|
119
|
+
return;
|
|
120
|
+
if (!this.clientId)
|
|
121
|
+
await this.ensureClientId();
|
|
122
|
+
this.connecting = true;
|
|
123
|
+
const headers = {
|
|
124
|
+
'cc-key': this.apiKey
|
|
125
|
+
};
|
|
126
|
+
const url = `${this.baseUrl}/run/clients/${this.clientId}/events`;
|
|
127
|
+
const emitAll = (event, payload) => {
|
|
128
|
+
for (const ch of this.sessions.values()) {
|
|
129
|
+
ch.emitter.emit(event, payload);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
this.conn = openSSE(url, {
|
|
134
|
+
onOpen: () => {
|
|
135
|
+
this.connected = true;
|
|
136
|
+
this.connecting = false;
|
|
137
|
+
emitAll('open');
|
|
138
|
+
},
|
|
139
|
+
onEvent: (evt) => {
|
|
140
|
+
if (evt.event === 'ping') {
|
|
141
|
+
emitAll('ping', evt);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (evt.event === 'run.event') {
|
|
145
|
+
const raw = evt;
|
|
146
|
+
const data = raw.data?.data;
|
|
147
|
+
if (!data) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Extract session_id - it's always present in payload
|
|
151
|
+
const payload = data.payload;
|
|
152
|
+
const sessionId = payload?.session_id;
|
|
153
|
+
if (!sessionId) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const channel = this.sessions.get(sessionId);
|
|
157
|
+
if (!channel) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const msg = {
|
|
161
|
+
event: 'run.event',
|
|
162
|
+
data: data,
|
|
163
|
+
timestamp: raw.timestamp || new Date().toISOString(),
|
|
164
|
+
expires_at: raw.expires_at || new Date(Date.now() + 3600000).toISOString()
|
|
165
|
+
};
|
|
166
|
+
// fan-out to all subscribers
|
|
167
|
+
for (const q of channel.subscribers)
|
|
168
|
+
q.push(msg);
|
|
169
|
+
channel.emitter.emit('run.event', msg);
|
|
170
|
+
const eventType = data.event;
|
|
171
|
+
if (isFinalEvent(eventType)) {
|
|
172
|
+
channel.ended = true;
|
|
173
|
+
channel.emitter.emit('end', { type: eventType });
|
|
174
|
+
for (const q of channel.subscribers)
|
|
175
|
+
q.close();
|
|
176
|
+
channel.subscribers.clear();
|
|
177
|
+
// Remove the channel after notifying
|
|
178
|
+
this.sessions.delete(sessionId);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const u = evt;
|
|
183
|
+
},
|
|
184
|
+
onError: (err) => {
|
|
185
|
+
// Surface error to all channels and attempt reconnect
|
|
186
|
+
emitAll('error', err);
|
|
187
|
+
if (!this.reconnecting)
|
|
188
|
+
this.scheduleReconnect();
|
|
189
|
+
},
|
|
190
|
+
onClose: () => {
|
|
191
|
+
this.connected = false;
|
|
192
|
+
this.connecting = false;
|
|
193
|
+
emitAll('close');
|
|
194
|
+
if (!this.reconnecting)
|
|
195
|
+
this.scheduleReconnect();
|
|
196
|
+
}
|
|
197
|
+
}, {
|
|
198
|
+
headers,
|
|
199
|
+
withCredentials: false
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
this.connected = false;
|
|
204
|
+
this.connecting = false;
|
|
205
|
+
if (!this.reconnecting)
|
|
206
|
+
this.scheduleReconnect();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
scheduleReconnect() {
|
|
210
|
+
if (this.reconnecting)
|
|
211
|
+
return;
|
|
212
|
+
this.reconnecting = true;
|
|
213
|
+
(async () => {
|
|
214
|
+
for (const delay of this.reconnectDelays) {
|
|
215
|
+
// Notify listeners about reconnect attempt
|
|
216
|
+
for (const ch of this.sessions.values())
|
|
217
|
+
ch.emitter.emit('reconnect', { attemptDelayMs: delay });
|
|
218
|
+
await new Promise(r => setTimeout(r, delay));
|
|
219
|
+
try {
|
|
220
|
+
await this.openMuxConnection();
|
|
221
|
+
if (this.connected) {
|
|
222
|
+
this.reconnecting = false;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// continue to next delay
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Give up after exhausting delays; next event/subscribe will try again
|
|
231
|
+
this.reconnecting = false;
|
|
232
|
+
})();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type EventHandler<T = unknown> = (event: T) => void;
|
|
2
|
+
/**
|
|
3
|
+
* Event emitter that supports both typed and untyped usage.
|
|
4
|
+
*
|
|
5
|
+
* - Use without type parameter for untyped events (backward compatible)
|
|
6
|
+
* - Use with EventMap type parameter for type-safe events
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Untyped usage
|
|
10
|
+
* const emitter = new SimpleEventEmitter();
|
|
11
|
+
* emitter.on('foo', (data) => console.log(data));
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Typed usage
|
|
15
|
+
* type Events = { foo: string; bar: number };
|
|
16
|
+
* const emitter = new SimpleEventEmitter<Events>();
|
|
17
|
+
* emitter.on('foo', (data) => console.log(data)); // data is string
|
|
18
|
+
*/
|
|
19
|
+
export declare class SimpleEventEmitter<EventMap extends Record<string, any> = Record<string, unknown>> {
|
|
20
|
+
private listeners;
|
|
21
|
+
on<K extends keyof EventMap>(event: K, handler: EventHandler<EventMap[K]>): () => void;
|
|
22
|
+
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void;
|
|
23
|
+
clear(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event emitter that supports both typed and untyped usage.
|
|
3
|
+
*
|
|
4
|
+
* - Use without type parameter for untyped events (backward compatible)
|
|
5
|
+
* - Use with EventMap type parameter for type-safe events
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Untyped usage
|
|
9
|
+
* const emitter = new SimpleEventEmitter();
|
|
10
|
+
* emitter.on('foo', (data) => console.log(data));
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Typed usage
|
|
14
|
+
* type Events = { foo: string; bar: number };
|
|
15
|
+
* const emitter = new SimpleEventEmitter<Events>();
|
|
16
|
+
* emitter.on('foo', (data) => console.log(data)); // data is string
|
|
17
|
+
*/
|
|
18
|
+
export class SimpleEventEmitter {
|
|
19
|
+
listeners = new Map();
|
|
20
|
+
on(event, handler) {
|
|
21
|
+
if (!this.listeners.has(event))
|
|
22
|
+
this.listeners.set(event, new Set());
|
|
23
|
+
this.listeners.get(event).add(handler);
|
|
24
|
+
return () => {
|
|
25
|
+
const set = this.listeners.get(event);
|
|
26
|
+
if (set)
|
|
27
|
+
set.delete(handler);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
emit(event, payload) {
|
|
31
|
+
const set = this.listeners.get(event);
|
|
32
|
+
if (!set)
|
|
33
|
+
return;
|
|
34
|
+
for (const handler of set)
|
|
35
|
+
handler(payload);
|
|
36
|
+
}
|
|
37
|
+
clear() {
|
|
38
|
+
this.listeners.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SSEHandlers {
|
|
2
|
+
onOpen?: () => void;
|
|
3
|
+
onEvent?: (evt: {
|
|
4
|
+
event: string;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
id?: string;
|
|
7
|
+
raw?: string;
|
|
8
|
+
}) => void;
|
|
9
|
+
onError?: (error: Error) => void;
|
|
10
|
+
onClose?: () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface SSEOptions {
|
|
13
|
+
headers?: HeadersInit;
|
|
14
|
+
withCredentials?: boolean;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
export interface SSEConnection {
|
|
18
|
+
close(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Open an SSE connection using either native EventSource (browser, cookie auth)
|
|
22
|
+
* or fetch streaming (Node 18+ and modern browsers) when custom headers are needed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function openSSE(url: string, handlers: SSEHandlers, opts?: SSEOptions): SSEConnection;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open an SSE connection using either native EventSource (browser, cookie auth)
|
|
3
|
+
* or fetch streaming (Node 18+ and modern browsers) when custom headers are needed.
|
|
4
|
+
*/
|
|
5
|
+
export function openSSE(url, handlers, opts) {
|
|
6
|
+
const { onOpen, onEvent, onError, onClose } = handlers;
|
|
7
|
+
const { headers, withCredentials, signal } = opts ?? {};
|
|
8
|
+
const shouldUseEventSource = () => typeof window !== 'undefined' &&
|
|
9
|
+
'EventSource' in window &&
|
|
10
|
+
withCredentials === true &&
|
|
11
|
+
(!headers || Object.keys(headers).length === 0);
|
|
12
|
+
const parseJSON = (text) => {
|
|
13
|
+
try {
|
|
14
|
+
return text ? JSON.parse(text) : undefined;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return text || undefined;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const createEventSourceConnection = () => {
|
|
21
|
+
const ES = window.EventSource;
|
|
22
|
+
const es = new ES(url, { withCredentials: true });
|
|
23
|
+
const onOpenHandler = () => onOpen?.();
|
|
24
|
+
const onErrorHandler = (e) => onError?.(e instanceof Error ? e : new Error('EventSource error'));
|
|
25
|
+
const onRunEvent = (e) => {
|
|
26
|
+
const raw = String(e.data ?? '');
|
|
27
|
+
const parsed = parseJSON(raw);
|
|
28
|
+
onEvent?.({ event: 'run.event', data: parsed, id: e.lastEventId, raw });
|
|
29
|
+
};
|
|
30
|
+
const onPing = (e) => {
|
|
31
|
+
const raw = String(e.data ?? '');
|
|
32
|
+
const parsed = parseJSON(raw);
|
|
33
|
+
onEvent?.({ event: 'ping', data: parsed, id: e.lastEventId, raw });
|
|
34
|
+
};
|
|
35
|
+
es.addEventListener('open', onOpenHandler);
|
|
36
|
+
es.addEventListener('error', onErrorHandler);
|
|
37
|
+
es.addEventListener('run.event', onRunEvent);
|
|
38
|
+
es.addEventListener('ping', onPing);
|
|
39
|
+
if (signal) {
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
es.close();
|
|
42
|
+
onClose?.();
|
|
43
|
+
};
|
|
44
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
close() {
|
|
48
|
+
es.close();
|
|
49
|
+
onClose?.();
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const createFetchConnection = () => {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const abort = () => controller.abort();
|
|
56
|
+
if (signal)
|
|
57
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
58
|
+
const parseFrame = (frame) => {
|
|
59
|
+
let event = 'message';
|
|
60
|
+
let data = '';
|
|
61
|
+
let id;
|
|
62
|
+
for (const line of frame.split(/\r?\n/)) {
|
|
63
|
+
if (!line || line.startsWith(':'))
|
|
64
|
+
continue;
|
|
65
|
+
const idx = line.indexOf(':');
|
|
66
|
+
const field = idx === -1 ? line : line.slice(0, idx);
|
|
67
|
+
const value = idx === -1 ? '' : line.slice(idx + 1).trimStart();
|
|
68
|
+
if (field === 'event')
|
|
69
|
+
event = value;
|
|
70
|
+
else if (field === 'data')
|
|
71
|
+
data += (data ? '\n' : '') + value;
|
|
72
|
+
else if (field === 'id')
|
|
73
|
+
id = value;
|
|
74
|
+
}
|
|
75
|
+
const parsed = parseJSON(data);
|
|
76
|
+
return { event, data: parsed, id, raw: frame };
|
|
77
|
+
};
|
|
78
|
+
(async () => {
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(url, {
|
|
81
|
+
method: 'GET',
|
|
82
|
+
headers: {
|
|
83
|
+
Accept: 'text/event-stream',
|
|
84
|
+
'Cache-Control': 'no-cache',
|
|
85
|
+
...(headers ?? {}),
|
|
86
|
+
},
|
|
87
|
+
credentials: withCredentials ? 'include' : 'same-origin',
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok || !res.body)
|
|
91
|
+
throw new Error(`SSE HTTP ${res.status}`);
|
|
92
|
+
onOpen?.();
|
|
93
|
+
const reader = res.body.getReader();
|
|
94
|
+
const decoder = new TextDecoder();
|
|
95
|
+
let buffer = '';
|
|
96
|
+
while (true) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done)
|
|
99
|
+
break;
|
|
100
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
101
|
+
const parts = (buffer + chunk).split(/\r?\n\r?\n/);
|
|
102
|
+
buffer = parts.pop() ?? '';
|
|
103
|
+
for (const frame of parts) {
|
|
104
|
+
const evt = parseFrame(frame);
|
|
105
|
+
onEvent?.(evt);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
onClose?.();
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
return {
|
|
115
|
+
close() {
|
|
116
|
+
abort();
|
|
117
|
+
onClose?.();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
return shouldUseEventSource() ? createEventSourceConnection() : createFetchConnection();
|
|
122
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { GetVaultEntriesFilters, VaultEntry, VaultTfaCode } from './types.js';
|
|
2
|
+
export declare class VaultClient {
|
|
3
|
+
private readonly makeRequest;
|
|
4
|
+
private readonly encryptionKey;
|
|
5
|
+
constructor(makeRequest: <T = any>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: any) => Promise<T>, encryptionKey: string);
|
|
6
|
+
/**
|
|
7
|
+
* Creates a new vault entry
|
|
8
|
+
*/
|
|
9
|
+
create(domain: string, permissioned_user_id: string, options?: Partial<Omit<VaultEntry, 'id' | 'created_at' | 'domain' | 'permissioned_user_id'>>): Promise<VaultEntry>;
|
|
10
|
+
/**
|
|
11
|
+
* Gets vault entries, optionally filtered
|
|
12
|
+
* @param filters - Optional filters for the request
|
|
13
|
+
* @param filters.permissioned_user_id - Filter by user ID
|
|
14
|
+
* @param filters.domain - Filter by domain
|
|
15
|
+
* @param filters.decryptCredentials - Whether to decrypt sensitive fields (default: true)
|
|
16
|
+
*/
|
|
17
|
+
get(filters?: GetVaultEntriesFilters): Promise<VaultEntry[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Gets the current 2FA code for a single vault entry.
|
|
20
|
+
*
|
|
21
|
+
* The code returned depends on the credential's 2FA method:
|
|
22
|
+
* - Authenticator (TOTP): a freshly generated code, with `expires_in_seconds`.
|
|
23
|
+
* - Email: the most recently received code (within the freshness window), with `received_at`.
|
|
24
|
+
*
|
|
25
|
+
* SMS and magic-link credentials are not supported (the endpoint returns 409).
|
|
26
|
+
*
|
|
27
|
+
* @param permissioned_user_id - User identifier for the vault entry
|
|
28
|
+
* @param domain - Target domain of the vault entry
|
|
29
|
+
*/
|
|
30
|
+
getTfaCode(permissioned_user_id: string, domain: string): Promise<VaultTfaCode>;
|
|
31
|
+
/**
|
|
32
|
+
* Updates an existing vault entry
|
|
33
|
+
* @param updates - Vault entry updates including required fields
|
|
34
|
+
* @param updates.permissioned_user_id - Required: User identifier for the vault entry
|
|
35
|
+
* @param updates.user_name - Required: Username or email
|
|
36
|
+
* @param updates.password - Required: User password
|
|
37
|
+
* @param updates.domain - Required: Target domain for the credentials
|
|
38
|
+
*/
|
|
39
|
+
update(updates: Partial<VaultEntry> & {
|
|
40
|
+
permissioned_user_id: string;
|
|
41
|
+
user_name: string;
|
|
42
|
+
password: string;
|
|
43
|
+
domain: string;
|
|
44
|
+
}): Promise<VaultEntry>;
|
|
45
|
+
/**
|
|
46
|
+
* Deletes a vault entry by domain and permissioned user ID
|
|
47
|
+
* @param params - Object containing domain and permissioned_user_id
|
|
48
|
+
* @param params.domain - The domain of the vault entry to delete
|
|
49
|
+
* @param params.permissioned_user_id - The permissioned user ID of the vault entry to delete
|
|
50
|
+
*/
|
|
51
|
+
delete(params: {
|
|
52
|
+
domain: string;
|
|
53
|
+
permissioned_user_id: string;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { encryptSensitiveFields, decryptSensitiveFields } from './utils.js';
|
|
2
|
+
export class VaultClient {
|
|
3
|
+
makeRequest;
|
|
4
|
+
encryptionKey;
|
|
5
|
+
constructor(makeRequest, encryptionKey) {
|
|
6
|
+
this.makeRequest = makeRequest;
|
|
7
|
+
this.encryptionKey = encryptionKey;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new vault entry
|
|
11
|
+
*/
|
|
12
|
+
async create(domain, permissioned_user_id, options) {
|
|
13
|
+
const entry = {
|
|
14
|
+
domain,
|
|
15
|
+
permissioned_user_id,
|
|
16
|
+
...options
|
|
17
|
+
};
|
|
18
|
+
let processedEntry = { ...entry };
|
|
19
|
+
// Encrypt sensitive fields
|
|
20
|
+
processedEntry = await encryptSensitiveFields(processedEntry, this.encryptionKey);
|
|
21
|
+
const response = await this.makeRequest('POST', '/vault', processedEntry);
|
|
22
|
+
// Decrypt response using encryption key
|
|
23
|
+
return await decryptSensitiveFields(response, this.encryptionKey);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Gets vault entries, optionally filtered
|
|
27
|
+
* @param filters - Optional filters for the request
|
|
28
|
+
* @param filters.permissioned_user_id - Filter by user ID
|
|
29
|
+
* @param filters.domain - Filter by domain
|
|
30
|
+
* @param filters.decryptCredentials - Whether to decrypt sensitive fields (default: true)
|
|
31
|
+
*/
|
|
32
|
+
async get(filters) {
|
|
33
|
+
let path = '/vault';
|
|
34
|
+
if (filters && (filters.permissioned_user_id || filters.domain)) {
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
if (filters.permissioned_user_id) {
|
|
37
|
+
params.append('permissioned_user_id', filters.permissioned_user_id);
|
|
38
|
+
}
|
|
39
|
+
if (filters.domain) {
|
|
40
|
+
params.append('domain', filters.domain);
|
|
41
|
+
}
|
|
42
|
+
path += `?${params.toString()}`;
|
|
43
|
+
}
|
|
44
|
+
const response = await this.makeRequest('GET', path);
|
|
45
|
+
let entries = Array.isArray(response) ? response : [response];
|
|
46
|
+
// Conditionally decrypt sensitive fields based on decryptCredentials flag
|
|
47
|
+
const shouldDecrypt = filters?.decryptCredentials !== false;
|
|
48
|
+
if (shouldDecrypt) {
|
|
49
|
+
entries = await Promise.all(entries.map(entry => decryptSensitiveFields(entry, this.encryptionKey)));
|
|
50
|
+
}
|
|
51
|
+
return entries;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Gets the current 2FA code for a single vault entry.
|
|
55
|
+
*
|
|
56
|
+
* The code returned depends on the credential's 2FA method:
|
|
57
|
+
* - Authenticator (TOTP): a freshly generated code, with `expires_in_seconds`.
|
|
58
|
+
* - Email: the most recently received code (within the freshness window), with `received_at`.
|
|
59
|
+
*
|
|
60
|
+
* SMS and magic-link credentials are not supported (the endpoint returns 409).
|
|
61
|
+
*
|
|
62
|
+
* @param permissioned_user_id - User identifier for the vault entry
|
|
63
|
+
* @param domain - Target domain of the vault entry
|
|
64
|
+
*/
|
|
65
|
+
async getTfaCode(permissioned_user_id, domain) {
|
|
66
|
+
if (!permissioned_user_id) {
|
|
67
|
+
throw new Error('permissioned_user_id is required to get a TFA code');
|
|
68
|
+
}
|
|
69
|
+
if (!domain) {
|
|
70
|
+
throw new Error('domain is required to get a TFA code');
|
|
71
|
+
}
|
|
72
|
+
const params = new URLSearchParams();
|
|
73
|
+
params.append('permissioned_user_id', permissioned_user_id);
|
|
74
|
+
params.append('domain', domain);
|
|
75
|
+
return await this.makeRequest('GET', `/vault/tfa-code?${params.toString()}`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Updates an existing vault entry
|
|
79
|
+
* @param updates - Vault entry updates including required fields
|
|
80
|
+
* @param updates.permissioned_user_id - Required: User identifier for the vault entry
|
|
81
|
+
* @param updates.user_name - Required: Username or email
|
|
82
|
+
* @param updates.password - Required: User password
|
|
83
|
+
* @param updates.domain - Required: Target domain for the credentials
|
|
84
|
+
*/
|
|
85
|
+
async update(updates) {
|
|
86
|
+
// Validate required fields
|
|
87
|
+
if (!updates.permissioned_user_id) {
|
|
88
|
+
throw new Error('permissioned_user_id is required for vault updates');
|
|
89
|
+
}
|
|
90
|
+
if (!updates.user_name) {
|
|
91
|
+
throw new Error('user_name is required for vault updates');
|
|
92
|
+
}
|
|
93
|
+
if (!updates.password) {
|
|
94
|
+
throw new Error('password is required for vault updates');
|
|
95
|
+
}
|
|
96
|
+
if (!updates.domain) {
|
|
97
|
+
throw new Error('domain is required for vault updates');
|
|
98
|
+
}
|
|
99
|
+
let processedEntry = { ...updates };
|
|
100
|
+
// Encrypt sensitive fields
|
|
101
|
+
processedEntry = await encryptSensitiveFields(processedEntry, this.encryptionKey);
|
|
102
|
+
const response = await this.makeRequest('PUT', '/vault', processedEntry);
|
|
103
|
+
// Decrypt response using encryption key
|
|
104
|
+
return await decryptSensitiveFields(response, this.encryptionKey);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Deletes a vault entry by domain and permissioned user ID
|
|
108
|
+
* @param params - Object containing domain and permissioned_user_id
|
|
109
|
+
* @param params.domain - The domain of the vault entry to delete
|
|
110
|
+
* @param params.permissioned_user_id - The permissioned user ID of the vault entry to delete
|
|
111
|
+
*/
|
|
112
|
+
async delete(params) {
|
|
113
|
+
await this.makeRequest('DELETE', '/vault', params);
|
|
114
|
+
}
|
|
115
|
+
}
|