cloudcruise 1.0.1 → 1.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.
@@ -1,4 +1,5 @@
1
1
  import { VaultClient } from './vault/VaultClient.js';
2
+ import { SecretProvidersClient } from './secretProviders/SecretProvidersClient.js';
2
3
  import { WorkflowsClient } from './workflows/WorkflowsClient.js';
3
4
  import { RunsClient } from './runs/RunsClient.js';
4
5
  import { WebhookClient } from './webhook/WebhookClient.js';
@@ -6,7 +7,7 @@ export interface CloudCruiseParams {
6
7
  apiKey?: string;
7
8
  /**
8
9
  * CloudCruise API base URL. Authenticated requests are restricted to the
9
- * production CloudCruise API origin.
10
+ * production CloudCruise API origin or the staging API origin.
10
11
  */
11
12
  baseUrl?: string;
12
13
  encryptionKey?: string;
@@ -16,6 +17,7 @@ export declare class CloudCruise {
16
17
  private readonly baseUrl;
17
18
  private readonly encryptionKey;
18
19
  readonly vault: VaultClient;
20
+ readonly secretProviders: SecretProvidersClient;
19
21
  readonly workflows: WorkflowsClient;
20
22
  readonly runs: RunsClient;
21
23
  readonly webhook: WebhookClient;
@@ -1,10 +1,13 @@
1
1
  import { getEnv } from './utils/env.js';
2
2
  import { VaultClient } from './vault/VaultClient.js';
3
+ import { SecretProvidersClient } from './secretProviders/SecretProvidersClient.js';
3
4
  import { WorkflowsClient } from './workflows/WorkflowsClient.js';
4
5
  import { RunsClient } from './runs/RunsClient.js';
5
6
  import { WebhookClient } from './webhook/WebhookClient.js';
6
7
  import { ConnectionManager } from './utils/connectionManager.js';
7
8
  const DEFAULT_BASE_URL = 'https://api.cloudcruise.com';
9
+ const STAGING_BASE_URL = 'https://staging-api.cloudcruise.app';
10
+ const ALLOWED_BASE_URLS = new Set([DEFAULT_BASE_URL, STAGING_BASE_URL]);
8
11
  const DEFAULT_API_HOST = new URL(DEFAULT_BASE_URL).host.toLowerCase();
9
12
  function normalizeBaseUrl(baseUrl) {
10
13
  let url;
@@ -27,9 +30,9 @@ function assertBaseUrlAllowed(baseUrl) {
27
30
  if (host === DEFAULT_API_HOST && url.protocol !== 'https:') {
28
31
  throw new Error(`Refusing to send CloudCruise API key to "${baseUrl}". The default CloudCruise API host requires https:.`);
29
32
  }
30
- if (baseUrl !== DEFAULT_BASE_URL) {
33
+ if (!ALLOWED_BASE_URLS.has(baseUrl)) {
31
34
  throw new Error(`Refusing to send CloudCruise API key to unapproved baseUrl "${baseUrl}". ` +
32
- `Authenticated requests are restricted to ${DEFAULT_BASE_URL}.`);
35
+ `Authenticated requests are restricted to: ${Array.from(ALLOWED_BASE_URLS).join(", ")}.`);
33
36
  }
34
37
  }
35
38
  export class CloudCruise {
@@ -37,6 +40,7 @@ export class CloudCruise {
37
40
  baseUrl;
38
41
  encryptionKey;
39
42
  vault;
43
+ secretProviders;
40
44
  workflows;
41
45
  runs;
42
46
  webhook;
@@ -59,6 +63,7 @@ export class CloudCruise {
59
63
  // Initialize namespace clients
60
64
  this.connectionManager = new ConnectionManager(this.baseUrl, this.apiKey);
61
65
  this.vault = new VaultClient(this.makeRequest.bind(this), this.encryptionKey);
66
+ this.secretProviders = new SecretProvidersClient(this.makeRequest.bind(this));
62
67
  this.workflows = new WorkflowsClient(this.makeRequest.bind(this));
63
68
  this.runs = new RunsClient(this.connectionManager, this.makeRequest.bind(this), this.workflows);
64
69
  this.webhook = new WebhookClient();
@@ -17,7 +17,8 @@ export declare enum EventType {
17
17
  InteractionWaiting = "interaction.waiting",
18
18
  InteractionFinished = "interaction.finished",
19
19
  InteractionFailed = "interaction.failed",
20
- AgentErrorAnalysis = "agent.error_analysis"
20
+ AgentErrorAnalysis = "agent.error_analysis",
21
+ ExecutionInputRequired = "execution.input_required"
21
22
  }
22
23
  export interface ExecutionQueuedPayload {
23
24
  session_id: string;
@@ -61,6 +62,37 @@ export interface AgentErrorAnalysisPayload {
61
62
  ai_analysis?: string;
62
63
  root_cause_analysis?: string;
63
64
  error_category?: string;
65
+ phase?: "modal_decision_dispatched" | "popup_dismiss_verified" | string;
66
+ session_id?: string;
67
+ modal_action?: string;
68
+ modal_action_label?: string;
69
+ response_time_ms?: number;
70
+ outcome?: "success" | "failure";
71
+ host?: string;
72
+ popup_signature?: string;
73
+ }
74
+ export interface AvailableAction {
75
+ id: string;
76
+ label: string;
77
+ }
78
+ export interface PopupRetry {
79
+ attempt: number;
80
+ max_attempts: number;
81
+ }
82
+ export interface PopupContext {
83
+ error_description: string;
84
+ error_sub_type?: string;
85
+ full_url?: string;
86
+ available_actions: AvailableAction[];
87
+ retry: PopupRetry;
88
+ }
89
+ export type InputRequiredReason = "input_required" | "incorrect_form_input" | "multiple_matching_results" | "non_dismissible_popup";
90
+ export interface ExecutionInputRequiredPayload {
91
+ session_id: string;
92
+ input_variables: Record<string, any>;
93
+ screenshot_url: string | null;
94
+ reason?: InputRequiredReason;
95
+ popup_context?: PopupContext;
64
96
  }
65
97
  export interface ExecutionRequeuedPayload {
66
98
  session_id: string;
@@ -132,6 +164,7 @@ export type EventPayloadMap = {
132
164
  [EventType.VideoUploaded]: never;
133
165
  [EventType.ExecutionPause]: never;
134
166
  [EventType.InteractionFailed]: never;
167
+ [EventType.ExecutionInputRequired]: ExecutionInputRequiredPayload;
135
168
  };
136
169
  export type WebhookMessage<E extends EventType = EventType> = {
137
170
  event: E;
@@ -19,4 +19,5 @@ export var EventType;
19
19
  EventType["InteractionFinished"] = "interaction.finished";
20
20
  EventType["InteractionFailed"] = "interaction.failed";
21
21
  EventType["AgentErrorAnalysis"] = "agent.error_analysis";
22
+ EventType["ExecutionInputRequired"] = "execution.input_required";
22
23
  })(EventType || (EventType = {}));
package/dist/index.d.ts CHANGED
@@ -5,14 +5,15 @@
5
5
  export { CloudCruise } from './CloudCruise.js';
6
6
  export type { CloudCruiseParams } from './CloudCruise.js';
7
7
  export { VaultClient } from './vault/VaultClient.js';
8
+ export { SecretProvidersClient } from './secretProviders/SecretProvidersClient.js';
8
9
  export { WorkflowsClient } from './workflows/WorkflowsClient.js';
9
10
  export { RunsClient } from './runs/RunsClient.js';
10
11
  export { WebhookClient } from './webhook/WebhookClient.js';
11
- export type { VaultEntry, GetVaultEntriesFilters, ProxyConfig, VaultPostPutHeadersInBody } from './vault/types.js';
12
+ export type { VaultEntry, GetVaultEntriesFilters, ProxyConfig, VaultPostPutHeadersInBody, SecretProvider, SecretProviderItem, SecretProviderType } from './vault/types.js';
12
13
  export type { Workflow, WorkflowInputSchema, WorkflowMetadata } from './workflows/types.js';
13
14
  export type { DryRun, Metadata, RunSpecificWebhook, PayloadWebhook, StartRunRequest, StartRunResponse, UserInteractionData, VideoUrl, SignedFileUrl, SignedScreenshotUrl, RunError, WorkflowError, RunResult, GetRunResult, WebhookEvent, WebhookReplayResponse, RunHandle, RunStreamOptions, SseEventName, SseMessage, RunEventEnvelope, RunHandleEventMap } from './runs/types.js';
14
15
  export { EventType } from './events/types.js';
15
- export type { WebhookMessage, RunEventMessage } from './events/types.js';
16
+ export type { WebhookMessage, RunEventMessage, AvailableAction, PopupRetry, PopupContext, InputRequiredReason, ExecutionInputRequiredPayload, AgentErrorAnalysisPayload } from './events/types.js';
16
17
  export type { WebhookVerificationOptions } from './webhook/types.js';
17
18
  export { VerificationError } from './webhook/types.js';
18
19
  export { InputValidationError } from './workflows/types.js';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export { CloudCruise } from './CloudCruise.js';
6
6
  export { VaultClient } from './vault/VaultClient.js';
7
+ export { SecretProvidersClient } from './secretProviders/SecretProvidersClient.js';
7
8
  export { WorkflowsClient } from './workflows/WorkflowsClient.js';
8
9
  export { RunsClient } from './runs/RunsClient.js';
9
10
  export { WebhookClient } from './webhook/WebhookClient.js';
@@ -1,4 +1,5 @@
1
1
  import type { StartRunRequest, UserInteractionData, GetRunResult, WebhookReplayResponse, RunHandle, RunStreamOptions } from './types.js';
2
+ import type { PopupContext, ExecutionInputRequiredPayload } from '../events/types.js';
2
3
  import { ConnectionManager } from '../utils/connectionManager.js';
3
4
  export declare class RunsClient {
4
5
  private readonly makeRequest;
@@ -22,6 +23,64 @@ export declare class RunsClient {
22
23
  * @param data - User input data as key-value pairs
23
24
  */
24
25
  submitUserInteraction(sessionId: string, data: UserInteractionData): Promise<void>;
26
+ /**
27
+ * Responds to an execution.input_required event whose reason is
28
+ * "non_dismissible_popup" by picking one of the CTA buttons surfaced in
29
+ * popup_context.available_actions. The backend dispatches a synthetic
30
+ * click on the chosen button and resumes the workflow.
31
+ *
32
+ * Only valid while the session is waiting for input. The backing endpoint
33
+ * returns 400 if the wait already expired (the workspace setting
34
+ * input_required_timeout_seconds, default 15s, max 300s).
35
+ *
36
+ * @param sessionId - The session waiting for input.
37
+ * @param actionId - One of the ids in popup_context.available_actions.
38
+ */
39
+ submitModalAction(sessionId: string, actionId: string): Promise<void>;
40
+ /**
41
+ * Responds to an execution.input_required event whose reason is
42
+ * "input_required", "incorrect_form_input", or "multiple_matching_results"
43
+ * by supplying the corrected/required input variables. Backend resumes from
44
+ * the appropriate recovery node with the new values substituted in.
45
+ *
46
+ * Mutually exclusive with submitModalAction at the endpoint level.
47
+ *
48
+ * @param sessionId - The session waiting for input.
49
+ * @param inputVariables - Mapping of variable name to new value.
50
+ */
51
+ submitInputVariables(sessionId: string, inputVariables: Record<string, any>): Promise<void>;
52
+ /**
53
+ * Registers a listener that auto-responds ONLY to non-dismissible modal
54
+ * input_required events (reason === "non_dismissible_popup"). The decider
55
+ * receives the popup_context and must return one of the action ids in
56
+ * popup_context.available_actions.
57
+ *
58
+ * The SDK never picks an action on its own. The customer's decider IS the
59
+ * decision point. If decider throws, the listener swallows it and skips
60
+ * submission; the backend's input wait will time out naturally.
61
+ *
62
+ * Other input_required reasons (incorrect_form_input, etc.) are ignored
63
+ * here and should be routed to onInputVariablesRequired.
64
+ *
65
+ * @returns An unsubscribe callable.
66
+ */
67
+ onPopupDecisionRequired(handle: RunHandle, decider: (ctx: PopupContext) => string | Promise<string>, onError?: (err: unknown) => void, options?: {
68
+ verbose?: boolean;
69
+ }): () => void;
70
+ /**
71
+ * Registers a listener that auto-responds ONLY to workflow-variable
72
+ * input_required events (reason in {"input_required",
73
+ * "incorrect_form_input", "multiple_matching_results"}). The decider
74
+ * receives the full payload and must return the input_variables dict.
75
+ *
76
+ * Counterpart to onPopupDecisionRequired. Modal events
77
+ * (reason === "non_dismissible_popup") are routed there and ignored here.
78
+ *
79
+ * @returns An unsubscribe callable.
80
+ */
81
+ onInputVariablesRequired(handle: RunHandle, decider: (payload: ExecutionInputRequiredPayload) => Record<string, any> | Promise<Record<string, any>>, onError?: (err: unknown) => void, options?: {
82
+ verbose?: boolean;
83
+ }): () => void;
25
84
  /**
26
85
  * Retrieves comprehensive results and execution details for a specific run
27
86
  * @param sessionId - The unique identifier for the workflow execution session
@@ -1,6 +1,37 @@
1
1
  import { EventType } from '../events/types.js';
2
2
  import { AsyncEventQueue } from '../utils/asyncQueue.js';
3
3
  import { SimpleEventEmitter } from '../utils/events.js';
4
+ /**
5
+ * Default error reporter for the recovery helpers. Writes to console.error
6
+ * so submission failures surface in customer logs even if no onError
7
+ * callback is provided. The runtime SimpleEventEmitter ignores async-handler
8
+ * returns, so without this default a rejection from /new_input_variables
9
+ * would become a silent unhandled rejection.
10
+ */
11
+ function defaultRecoverySubmitErrorLog(operation) {
12
+ return (err) => {
13
+ try {
14
+ const msg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
15
+ console.error(`[CloudCruise SDK] ${operation} failed during recovery: ${msg}`);
16
+ }
17
+ catch {
18
+ // never throw from the error reporter
19
+ }
20
+ };
21
+ }
22
+ /**
23
+ * Helper for `verbose: true` mode on the recovery helpers. Writes a single
24
+ * line to console.error so customers can watch the recovery loop without
25
+ * instrumenting their own decider.
26
+ */
27
+ function verboseLog(operation, message) {
28
+ try {
29
+ console.error(`[CloudCruise SDK verbose] ${operation}: ${message}`);
30
+ }
31
+ catch {
32
+ // never throw
33
+ }
34
+ }
4
35
  export class RunsClient {
5
36
  makeRequest;
6
37
  workflows;
@@ -171,6 +202,175 @@ export class RunsClient {
171
202
  const path = `/run/${sessionId}/user_interaction`;
172
203
  await this.makeRequest('POST', path, data);
173
204
  }
205
+ /**
206
+ * Responds to an execution.input_required event whose reason is
207
+ * "non_dismissible_popup" by picking one of the CTA buttons surfaced in
208
+ * popup_context.available_actions. The backend dispatches a synthetic
209
+ * click on the chosen button and resumes the workflow.
210
+ *
211
+ * Only valid while the session is waiting for input. The backing endpoint
212
+ * returns 400 if the wait already expired (the workspace setting
213
+ * input_required_timeout_seconds, default 15s, max 300s).
214
+ *
215
+ * @param sessionId - The session waiting for input.
216
+ * @param actionId - One of the ids in popup_context.available_actions.
217
+ */
218
+ async submitModalAction(sessionId, actionId) {
219
+ const path = `/run/${sessionId}/new_input_variables`;
220
+ await this.makeRequest('POST', path, { modal_action: actionId });
221
+ }
222
+ /**
223
+ * Responds to an execution.input_required event whose reason is
224
+ * "input_required", "incorrect_form_input", or "multiple_matching_results"
225
+ * by supplying the corrected/required input variables. Backend resumes from
226
+ * the appropriate recovery node with the new values substituted in.
227
+ *
228
+ * Mutually exclusive with submitModalAction at the endpoint level.
229
+ *
230
+ * @param sessionId - The session waiting for input.
231
+ * @param inputVariables - Mapping of variable name to new value.
232
+ */
233
+ async submitInputVariables(sessionId, inputVariables) {
234
+ const path = `/run/${sessionId}/new_input_variables`;
235
+ await this.makeRequest('POST', path, { input_variables: inputVariables });
236
+ }
237
+ /**
238
+ * Registers a listener that auto-responds ONLY to non-dismissible modal
239
+ * input_required events (reason === "non_dismissible_popup"). The decider
240
+ * receives the popup_context and must return one of the action ids in
241
+ * popup_context.available_actions.
242
+ *
243
+ * The SDK never picks an action on its own. The customer's decider IS the
244
+ * decision point. If decider throws, the listener swallows it and skips
245
+ * submission; the backend's input wait will time out naturally.
246
+ *
247
+ * Other input_required reasons (incorrect_form_input, etc.) are ignored
248
+ * here and should be routed to onInputVariablesRequired.
249
+ *
250
+ * @returns An unsubscribe callable.
251
+ */
252
+ onPopupDecisionRequired(handle, decider, onError, options) {
253
+ const verbose = options?.verbose === true;
254
+ const reportError = (err) => {
255
+ const reporter = onError ?? defaultRecoverySubmitErrorLog('submitModalAction');
256
+ try {
257
+ reporter(err);
258
+ }
259
+ catch {
260
+ // defense: a buggy onError must not crash the event loop
261
+ }
262
+ };
263
+ if (verbose)
264
+ verboseLog('onPopupDecisionRequired', 'listener registered');
265
+ const listener = async (event) => {
266
+ const payload = (event?.data?.payload ?? event?.payload);
267
+ if (!payload || payload.reason !== 'non_dismissible_popup' || !payload.popup_context) {
268
+ if (verbose) {
269
+ verboseLog('onPopupDecisionRequired', `skipping reason=${payload?.reason ?? 'undefined'}`);
270
+ }
271
+ return;
272
+ }
273
+ if (verbose) {
274
+ const attempt = payload.popup_context.retry?.attempt;
275
+ const actions = payload.popup_context.available_actions?.map((a) => a.id) ?? [];
276
+ verboseLog('onPopupDecisionRequired', `event received attempt=${attempt} actions=${JSON.stringify(actions)}`);
277
+ }
278
+ let actionId;
279
+ try {
280
+ actionId = await decider(payload.popup_context);
281
+ }
282
+ catch (e) {
283
+ if (verbose)
284
+ verboseLog('onPopupDecisionRequired', `decider raised: ${e}`);
285
+ return;
286
+ }
287
+ if (typeof actionId !== 'string' || actionId.length === 0) {
288
+ if (verbose)
289
+ verboseLog('onPopupDecisionRequired', `decider returned non-string/empty (${JSON.stringify(actionId)}); skipping`);
290
+ return;
291
+ }
292
+ const sid = payload.session_id || handle.sessionId;
293
+ if (verbose)
294
+ verboseLog('onPopupDecisionRequired', `submitting modal_action=${JSON.stringify(actionId)} for session=${sid}`);
295
+ try {
296
+ await this.submitModalAction(sid, actionId);
297
+ if (verbose)
298
+ verboseLog('onPopupDecisionRequired', `submit ok for action=${JSON.stringify(actionId)}`);
299
+ }
300
+ catch (err) {
301
+ reportError(err);
302
+ }
303
+ };
304
+ const unsubscribe = handle.on(EventType.ExecutionInputRequired, listener);
305
+ return typeof unsubscribe === 'function' ? unsubscribe : () => { };
306
+ }
307
+ /**
308
+ * Registers a listener that auto-responds ONLY to workflow-variable
309
+ * input_required events (reason in {"input_required",
310
+ * "incorrect_form_input", "multiple_matching_results"}). The decider
311
+ * receives the full payload and must return the input_variables dict.
312
+ *
313
+ * Counterpart to onPopupDecisionRequired. Modal events
314
+ * (reason === "non_dismissible_popup") are routed there and ignored here.
315
+ *
316
+ * @returns An unsubscribe callable.
317
+ */
318
+ onInputVariablesRequired(handle, decider, onError, options) {
319
+ const verbose = options?.verbose === true;
320
+ const VARIABLE_REASONS = new Set([
321
+ 'input_required',
322
+ 'incorrect_form_input',
323
+ 'multiple_matching_results',
324
+ ]);
325
+ const reportError = (err) => {
326
+ const reporter = onError ?? defaultRecoverySubmitErrorLog('submitInputVariables');
327
+ try {
328
+ reporter(err);
329
+ }
330
+ catch {
331
+ // defense
332
+ }
333
+ };
334
+ if (verbose)
335
+ verboseLog('onInputVariablesRequired', 'listener registered');
336
+ const listener = async (event) => {
337
+ const payload = (event?.data?.payload ?? event?.payload);
338
+ if (!payload || !payload.reason || !VARIABLE_REASONS.has(payload.reason)) {
339
+ if (verbose)
340
+ verboseLog('onInputVariablesRequired', `skipping reason=${payload?.reason ?? 'undefined'}`);
341
+ return;
342
+ }
343
+ if (verbose)
344
+ verboseLog('onInputVariablesRequired', `event received reason=${payload.reason}`);
345
+ let inputVars;
346
+ try {
347
+ inputVars = await decider(payload);
348
+ }
349
+ catch (e) {
350
+ if (verbose)
351
+ verboseLog('onInputVariablesRequired', `decider raised: ${e}`);
352
+ return;
353
+ }
354
+ if (!inputVars || typeof inputVars !== 'object' || Array.isArray(inputVars)) {
355
+ if (verbose)
356
+ verboseLog('onInputVariablesRequired', `decider returned non-object (${Array.isArray(inputVars) ? 'array' : typeof inputVars}); skipping`);
357
+ return;
358
+ }
359
+ const sid = payload.session_id || handle.sessionId;
360
+ if (verbose)
361
+ verboseLog('onInputVariablesRequired', `submitting input_variables keys=${JSON.stringify(Object.keys(inputVars))} for session=${sid}`);
362
+ try {
363
+ await this.submitInputVariables(sid, inputVars);
364
+ if (verbose)
365
+ verboseLog('onInputVariablesRequired', 'submit ok');
366
+ }
367
+ catch (err) {
368
+ reportError(err);
369
+ }
370
+ };
371
+ const unsubscribe = handle.on(EventType.ExecutionInputRequired, listener);
372
+ return typeof unsubscribe === 'function' ? unsubscribe : () => { };
373
+ }
174
374
  /**
175
375
  * Retrieves comprehensive results and execution details for a specific run
176
376
  * @param sessionId - The unique identifier for the workflow execution session
@@ -0,0 +1,7 @@
1
+ import type { SecretProvider, SecretProviderItem } from '../vault/types.js';
2
+ export declare class SecretProvidersClient {
3
+ private readonly makeRequest;
4
+ constructor(makeRequest: <T = any>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: any) => Promise<T>);
5
+ list(): Promise<SecretProvider[]>;
6
+ listItems(secretProviderId: string): Promise<SecretProviderItem[]>;
7
+ }
@@ -0,0 +1,17 @@
1
+ export class SecretProvidersClient {
2
+ makeRequest;
3
+ constructor(makeRequest) {
4
+ this.makeRequest = makeRequest;
5
+ }
6
+ async list() {
7
+ const response = await this.makeRequest('GET', '/secret-providers');
8
+ return Array.isArray(response) ? response : [response];
9
+ }
10
+ async listItems(secretProviderId) {
11
+ if (!secretProviderId) {
12
+ throw new Error('secretProviderId is required');
13
+ }
14
+ const response = await this.makeRequest('GET', `/secret-providers/${encodeURIComponent(secretProviderId)}/items`);
15
+ return Array.isArray(response) ? response : [response];
16
+ }
17
+ }
@@ -32,14 +32,10 @@ export declare class VaultClient {
32
32
  * Updates an existing vault entry
33
33
  * @param updates - Vault entry updates including required fields
34
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
35
  * @param updates.domain - Required: Target domain for the credentials
38
36
  */
39
37
  update(updates: Partial<VaultEntry> & {
40
38
  permissioned_user_id: string;
41
- user_name: string;
42
- password: string;
43
39
  domain: string;
44
40
  }): Promise<VaultEntry>;
45
41
  /**
@@ -1,4 +1,28 @@
1
1
  import { encryptSensitiveFields, decryptSensitiveFields } from './utils.js';
2
+ function validateProviderPayload(entry) {
3
+ const hasProviderId = entry.secret_provider_id !== undefined && entry.secret_provider_id !== null;
4
+ const hasSecretRef = entry.secret_ref !== undefined && entry.secret_ref !== null;
5
+ if (hasProviderId !== hasSecretRef) {
6
+ throw new Error('secret_provider_id and secret_ref must be provided together');
7
+ }
8
+ if (entry.secret_cache_ttl_seconds !== undefined && entry.secret_cache_ttl_seconds !== null) {
9
+ if (!Number.isInteger(entry.secret_cache_ttl_seconds) || entry.secret_cache_ttl_seconds < 0) {
10
+ throw new Error('secret_cache_ttl_seconds must be a non-negative integer');
11
+ }
12
+ if (!hasProviderId) {
13
+ throw new Error('secret_cache_ttl_seconds requires secret_provider_id and secret_ref');
14
+ }
15
+ }
16
+ if (hasProviderId) {
17
+ const conflicts = ['user_name', 'password', 'tfa_secret'].filter(field => entry[field] !== undefined && entry[field] !== null);
18
+ if (conflicts.length > 0) {
19
+ throw new Error(`provider-backed vault entries cannot include ${conflicts.join(', ')}`);
20
+ }
21
+ }
22
+ }
23
+ function isProviderBackedPayload(entry) {
24
+ return entry.secret_provider_id != null && entry.secret_ref != null;
25
+ }
2
26
  export class VaultClient {
3
27
  makeRequest;
4
28
  encryptionKey;
@@ -15,6 +39,7 @@ export class VaultClient {
15
39
  permissioned_user_id,
16
40
  ...options
17
41
  };
42
+ validateProviderPayload(entry);
18
43
  let processedEntry = { ...entry };
19
44
  // Encrypt sensitive fields
20
45
  processedEntry = await encryptSensitiveFields(processedEntry, this.encryptionKey);
@@ -78,8 +103,6 @@ export class VaultClient {
78
103
  * Updates an existing vault entry
79
104
  * @param updates - Vault entry updates including required fields
80
105
  * @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
106
  * @param updates.domain - Required: Target domain for the credentials
84
107
  */
85
108
  async update(updates) {
@@ -87,15 +110,18 @@ export class VaultClient {
87
110
  if (!updates.permissioned_user_id) {
88
111
  throw new Error('permissioned_user_id is required for vault updates');
89
112
  }
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
113
  if (!updates.domain) {
97
114
  throw new Error('domain is required for vault updates');
98
115
  }
116
+ validateProviderPayload(updates);
117
+ if (!isProviderBackedPayload(updates)) {
118
+ if (!updates.user_name) {
119
+ throw new Error('user_name is required for vault updates');
120
+ }
121
+ if (!updates.password) {
122
+ throw new Error('password is required for vault updates');
123
+ }
124
+ }
99
125
  let processedEntry = { ...updates };
100
126
  // Encrypt sensitive fields
101
127
  processedEntry = await encryptSensitiveFields(processedEntry, this.encryptionKey);
@@ -17,6 +17,9 @@ export interface VaultEntry {
17
17
  user_id?: string;
18
18
  password?: string;
19
19
  user_name?: string;
20
+ secret_provider_id?: string | null;
21
+ secret_ref?: string | null;
22
+ secret_cache_ttl_seconds?: number | null;
20
23
  tfa_secret?: string;
21
24
  user_agent?: string;
22
25
  user_alias?: string;
@@ -38,6 +41,8 @@ export interface VaultEntry {
38
41
  cookie_domain_to_store?: string | null;
39
42
  proxy?: ProxyConfig;
40
43
  proxy_string?: string | null;
44
+ proxy_setting?: 'random' | 'static' | 'country' | 'custom' | null;
45
+ proxy_value?: string | null;
41
46
  headers?: VaultPostPutHeadersInBody[];
42
47
  created_at?: string | null;
43
48
  session_data_set_at?: string | null;
@@ -48,6 +53,19 @@ export interface GetVaultEntriesFilters {
48
53
  domain?: string;
49
54
  decryptCredentials?: boolean;
50
55
  }
56
+ export type SecretProviderType = '1password';
57
+ export interface SecretProvider {
58
+ id: string;
59
+ provider_type: SecretProviderType;
60
+ name: string;
61
+ cache_ttl_seconds?: number | null;
62
+ }
63
+ export interface SecretProviderItem {
64
+ id: string;
65
+ title: string;
66
+ ref: string;
67
+ vaultName?: string | null;
68
+ }
51
69
  /**
52
70
  * The current 2FA code for a vault entry. `expires_in_seconds` is present for
53
71
  * authenticator (TOTP) codes; `received_at` is present for email codes.
@@ -18,7 +18,8 @@ export declare function encryptData(data: any, keyHex: string): Promise<string>;
18
18
  export declare function decryptData(encryptedHex: string, keyHex: string): Promise<any>;
19
19
  /**
20
20
  * Encrypts sensitive fields in a vault entry
21
- * Fields encrypted: user_name, password, tfa_secret (if present)
21
+ * Fields encrypted: user_name, password, tfa_secret (if present), and
22
+ * proxy_value when proxy_setting is "custom" (the bring-your-own proxy URL).
22
23
  * @param entry - Vault entry with potentially sensitive data
23
24
  * @param encryptionKey - Hex-encoded encryption key
24
25
  * @returns Entry with encrypted sensitive fields
@@ -51,7 +51,8 @@ export async function decryptData(encryptedHex, keyHex) {
51
51
  }
52
52
  /**
53
53
  * Encrypts sensitive fields in a vault entry
54
- * Fields encrypted: user_name, password, tfa_secret (if present)
54
+ * Fields encrypted: user_name, password, tfa_secret (if present), and
55
+ * proxy_value when proxy_setting is "custom" (the bring-your-own proxy URL).
55
56
  * @param entry - Vault entry with potentially sensitive data
56
57
  * @param encryptionKey - Hex-encoded encryption key
57
58
  * @returns Entry with encrypted sensitive fields
@@ -67,6 +68,22 @@ export async function encryptSensitiveFields(entry, encryptionKey) {
67
68
  if (entry.tfa_secret !== undefined) {
68
69
  encryptedEntry.tfa_secret = await encryptData(entry.tfa_secret, encryptionKey);
69
70
  }
71
+ // proxy_value is meaningless to the backend without proxy_setting, which is
72
+ // also the discriminator that tells us whether to encrypt. Fail closed: a
73
+ // custom proxy URL (often with embedded credentials) must never be sent in
74
+ // plaintext because the caller forgot the mode on a partial update.
75
+ if (entry.proxy_value !== undefined &&
76
+ entry.proxy_value !== null &&
77
+ (entry.proxy_setting === undefined || entry.proxy_setting === null)) {
78
+ throw new Error("proxy_value requires proxy_setting. Pass proxy_setting: 'custom' for a " +
79
+ "bring-your-own proxy URL (so it is encrypted before sending), or " +
80
+ "'static'/'country' for a managed proxy.");
81
+ }
82
+ if (entry.proxy_setting === 'custom' &&
83
+ entry.proxy_value !== undefined &&
84
+ entry.proxy_value !== null) {
85
+ encryptedEntry.proxy_value = await encryptData(entry.proxy_value, encryptionKey);
86
+ }
70
87
  return encryptedEntry;
71
88
  }
72
89
  /**
@@ -95,5 +112,11 @@ export async function decryptSensitiveFields(entry, encryptionKey) {
95
112
  }
96
113
  catch { }
97
114
  }
115
+ if (entry.proxy_setting === 'custom' && typeof entry.proxy_value === 'string') {
116
+ try {
117
+ decryptedEntry.proxy_value = await decryptData(entry.proxy_value, encryptionKey);
118
+ }
119
+ catch { }
120
+ }
98
121
  return decryptedEntry;
99
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudcruise",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "The official CloudCruise JS/TS client.",
5
5
  "homepage": "https://github.com/CloudCruise/cloudcruise-js#readme",
6
6
  "bugs": {