cloudcruise 1.2.0 → 1.3.1
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.js +15 -2
- package/dist/index.d.ts +2 -2
- package/dist/runs/RunsClient.d.ts +13 -1
- package/dist/runs/RunsClient.js +15 -0
- package/dist/runs/types.d.ts +5 -0
- package/dist/workflows/WorkflowsClient.js +10 -71
- package/dist/workflows/types.d.ts +20 -4
- package/dist/workflows/types.js +3 -1
- package/dist/workflows/validation.d.ts +3 -0
- package/dist/workflows/validation.js +354 -0
- package/package.json +4 -1
package/dist/CloudCruise.js
CHANGED
|
@@ -5,6 +5,7 @@ import { WorkflowsClient } from './workflows/WorkflowsClient.js';
|
|
|
5
5
|
import { RunsClient } from './runs/RunsClient.js';
|
|
6
6
|
import { WebhookClient } from './webhook/WebhookClient.js';
|
|
7
7
|
import { ConnectionManager } from './utils/connectionManager.js';
|
|
8
|
+
import { inputValidationErrorFromResponse } from './workflows/validation.js';
|
|
8
9
|
const DEFAULT_BASE_URL = 'https://api.cloudcruise.com';
|
|
9
10
|
const STAGING_BASE_URL = 'https://staging-api.cloudcruise.app';
|
|
10
11
|
const ALLOWED_BASE_URLS = new Set([DEFAULT_BASE_URL, STAGING_BASE_URL]);
|
|
@@ -87,13 +88,25 @@ export class CloudCruise {
|
|
|
87
88
|
if (!response.ok) {
|
|
88
89
|
const errorText = await response.text();
|
|
89
90
|
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
91
|
+
let errorJson;
|
|
90
92
|
try {
|
|
91
|
-
|
|
92
|
-
errorMessage = errorJson.message || errorJson.error || errorMessage;
|
|
93
|
+
errorJson = JSON.parse(errorText);
|
|
93
94
|
}
|
|
94
95
|
catch {
|
|
95
96
|
// Use HTTP status if we can't parse error response
|
|
96
97
|
}
|
|
98
|
+
if (typeof errorJson === 'object' &&
|
|
99
|
+
errorJson !== null &&
|
|
100
|
+
!Array.isArray(errorJson)) {
|
|
101
|
+
const errorBody = errorJson;
|
|
102
|
+
const validationError = inputValidationErrorFromResponse(errorBody, body);
|
|
103
|
+
if (validationError)
|
|
104
|
+
throw validationError;
|
|
105
|
+
errorMessage =
|
|
106
|
+
(typeof errorBody.message === 'string' && errorBody.message) ||
|
|
107
|
+
(typeof errorBody.error === 'string' && errorBody.error) ||
|
|
108
|
+
errorMessage;
|
|
109
|
+
}
|
|
97
110
|
throw new Error(errorMessage);
|
|
98
111
|
}
|
|
99
112
|
const contentType = response.headers.get('content-type');
|
package/dist/index.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export { WorkflowsClient } from './workflows/WorkflowsClient.js';
|
|
|
10
10
|
export { RunsClient } from './runs/RunsClient.js';
|
|
11
11
|
export { WebhookClient } from './webhook/WebhookClient.js';
|
|
12
12
|
export type { VaultEntry, GetVaultEntriesFilters, ProxyConfig, VaultPostPutHeadersInBody, SecretProvider, SecretProviderItem, SecretProviderType } from './vault/types.js';
|
|
13
|
-
export type { Workflow, WorkflowInputSchema, WorkflowMetadata } from './workflows/types.js';
|
|
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';
|
|
13
|
+
export type { Workflow, WorkflowInputSchema, WorkflowVaultCredentialSchema, WorkflowVaultSchema, WorkflowMetadata, InvalidTypeDetail, SchemaErrorDetail } from './workflows/types.js';
|
|
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, LiveViewConnection } from './runs/types.js';
|
|
15
15
|
export { EventType } from './events/types.js';
|
|
16
16
|
export type { WebhookMessage, RunEventMessage, AvailableAction, PopupRetry, PopupContext, InputRequiredReason, ExecutionInputRequiredPayload, AgentErrorAnalysisPayload } from './events/types.js';
|
|
17
17
|
export type { WebhookVerificationOptions } from './webhook/types.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StartRunRequest, UserInteractionData, GetRunResult, WebhookReplayResponse, RunHandle, RunStreamOptions } from './types.js';
|
|
1
|
+
import type { StartRunRequest, UserInteractionData, GetRunResult, WebhookReplayResponse, RunHandle, RunStreamOptions, LiveViewConnection } from './types.js';
|
|
2
2
|
import type { PopupContext, ExecutionInputRequiredPayload } from '../events/types.js';
|
|
3
3
|
import { ConnectionManager } from '../utils/connectionManager.js';
|
|
4
4
|
export declare class RunsClient {
|
|
@@ -87,6 +87,18 @@ export declare class RunsClient {
|
|
|
87
87
|
* @returns Promise resolving to complete run results
|
|
88
88
|
*/
|
|
89
89
|
getResults(sessionId: string): Promise<GetRunResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Fetches a fresh live-view connection (viewer URL + auth token) for
|
|
92
|
+
* watching an active session's browser stream. The auth token is
|
|
93
|
+
* single-use, so reopening a previously used viewer link will fail —
|
|
94
|
+
* call this again to mint a new one instead.
|
|
95
|
+
*
|
|
96
|
+
* Only works while the session is still active; throws once the session
|
|
97
|
+
* has ended.
|
|
98
|
+
*
|
|
99
|
+
* @param sessionId - The unique identifier for the workflow execution session
|
|
100
|
+
*/
|
|
101
|
+
getLiveViewConnection(sessionId: string): Promise<LiveViewConnection>;
|
|
90
102
|
/**
|
|
91
103
|
* Interrupts a running browser agent run
|
|
92
104
|
* @param sessionId - The unique identifier for the workflow execution session
|
package/dist/runs/RunsClient.js
CHANGED
|
@@ -380,6 +380,21 @@ export class RunsClient {
|
|
|
380
380
|
const path = `/run/${sessionId}`;
|
|
381
381
|
return await this.makeRequest('GET', path);
|
|
382
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Fetches a fresh live-view connection (viewer URL + auth token) for
|
|
385
|
+
* watching an active session's browser stream. The auth token is
|
|
386
|
+
* single-use, so reopening a previously used viewer link will fail —
|
|
387
|
+
* call this again to mint a new one instead.
|
|
388
|
+
*
|
|
389
|
+
* Only works while the session is still active; throws once the session
|
|
390
|
+
* has ended.
|
|
391
|
+
*
|
|
392
|
+
* @param sessionId - The unique identifier for the workflow execution session
|
|
393
|
+
*/
|
|
394
|
+
async getLiveViewConnection(sessionId) {
|
|
395
|
+
const path = `/live/sessions/${sessionId}/connection`;
|
|
396
|
+
return await this.makeRequest('GET', path);
|
|
397
|
+
}
|
|
383
398
|
/**
|
|
384
399
|
* Interrupts a running browser agent run
|
|
385
400
|
* @param sessionId - The unique identifier for the workflow execution session
|
package/dist/runs/types.d.ts
CHANGED
|
@@ -36,6 +36,11 @@ export interface VideoUrl {
|
|
|
36
36
|
signed_screen_recording_url: string;
|
|
37
37
|
signed_screen_recording_url_expires: string;
|
|
38
38
|
}
|
|
39
|
+
export interface LiveViewConnection {
|
|
40
|
+
url: string;
|
|
41
|
+
sessionId: string;
|
|
42
|
+
authToken: string;
|
|
43
|
+
}
|
|
39
44
|
export interface RunError {
|
|
40
45
|
prompt?: string | null;
|
|
41
46
|
message?: string | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { validateInputSchema } from './validation.js';
|
|
2
2
|
export class WorkflowsClient {
|
|
3
3
|
makeRequest;
|
|
4
4
|
constructor(makeRequest) {
|
|
@@ -16,7 +16,14 @@ export class WorkflowsClient {
|
|
|
16
16
|
*/
|
|
17
17
|
async getWorkflowMetadata(workflowId) {
|
|
18
18
|
const path = `/workflows/${workflowId}/metadata`;
|
|
19
|
-
|
|
19
|
+
const response = await this.makeRequest('GET', path);
|
|
20
|
+
if (response &&
|
|
21
|
+
typeof response === 'object' &&
|
|
22
|
+
!('input_schema' in response) &&
|
|
23
|
+
response.metadata) {
|
|
24
|
+
return response.metadata;
|
|
25
|
+
}
|
|
26
|
+
return response;
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
29
|
* Validates a payload against a workflow's input schema.
|
|
@@ -24,74 +31,6 @@ export class WorkflowsClient {
|
|
|
24
31
|
*/
|
|
25
32
|
async validateWorkflowInput(workflowId, payload) {
|
|
26
33
|
const { input_schema } = await this.getWorkflowMetadata(workflowId);
|
|
27
|
-
|
|
28
|
-
const properties = schema.properties ?? {};
|
|
29
|
-
const required = schema.required ?? [];
|
|
30
|
-
const disallowExtras = schema.additionalProperties === false;
|
|
31
|
-
// Check only required keys for presence and type
|
|
32
|
-
const missingRequired = required.filter((key) => payload[key] === undefined);
|
|
33
|
-
const invalidTypes = [];
|
|
34
|
-
const detectType = (v) => {
|
|
35
|
-
if (v === null)
|
|
36
|
-
return 'null';
|
|
37
|
-
if (Array.isArray(v))
|
|
38
|
-
return 'array';
|
|
39
|
-
if (typeof v === 'number')
|
|
40
|
-
return Number.isInteger(v) ? 'integer' : 'number';
|
|
41
|
-
return typeof v;
|
|
42
|
-
};
|
|
43
|
-
const allowedTypes = new Set(['array', 'boolean', 'integer', 'number', 'object', 'string', 'null']);
|
|
44
|
-
const expectedTypesOf = (def) => {
|
|
45
|
-
if (!def)
|
|
46
|
-
return [];
|
|
47
|
-
// Normalize to array-of-strings from either string | string[] | { type: string | string[] }
|
|
48
|
-
const raw = (typeof def === 'object' && !Array.isArray(def)) ? def.type : def;
|
|
49
|
-
if (!raw)
|
|
50
|
-
return [];
|
|
51
|
-
const arr = Array.isArray(raw) ? raw : [raw];
|
|
52
|
-
return arr
|
|
53
|
-
.map((t) => String(t).toLowerCase())
|
|
54
|
-
.filter((t) => allowedTypes.has(t));
|
|
55
|
-
};
|
|
56
|
-
const matches = (expected, actual) => {
|
|
57
|
-
if (expected.length === 0)
|
|
58
|
-
return true; // unknown => don't enforce
|
|
59
|
-
if (expected.includes(actual))
|
|
60
|
-
return true;
|
|
61
|
-
if (actual === 'integer' && expected.includes('number'))
|
|
62
|
-
return true;
|
|
63
|
-
return false;
|
|
64
|
-
};
|
|
65
|
-
// Validate types for:
|
|
66
|
-
// - all required keys that are present
|
|
67
|
-
// - optional keys if they exist in the payload
|
|
68
|
-
for (const [key, schemaDef] of Object.entries(properties)) {
|
|
69
|
-
if (payload[key] === undefined)
|
|
70
|
-
continue; // optional and not provided
|
|
71
|
-
const expected = expectedTypesOf(schemaDef);
|
|
72
|
-
const actual = detectType(payload[key]);
|
|
73
|
-
if (!matches(expected, actual)) {
|
|
74
|
-
const exp = expected.length ? expected : ['any'];
|
|
75
|
-
invalidTypes.push({ field: key, expected_display: exp.join(' | '), actual });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// If additionalProperties is false, collect unknown keys present in payload
|
|
79
|
-
const unknownKeys = disallowExtras
|
|
80
|
-
? Object.keys(payload).filter((k) => !(k in properties))
|
|
81
|
-
: [];
|
|
82
|
-
if (missingRequired.length || invalidTypes.length || unknownKeys.length) {
|
|
83
|
-
const parts = [];
|
|
84
|
-
if (missingRequired.length)
|
|
85
|
-
parts.push(`missing required: ${missingRequired.join(', ')}`);
|
|
86
|
-
if (invalidTypes.length) {
|
|
87
|
-
parts.push(invalidTypes
|
|
88
|
-
.map((e) => `${e.field}: expected ${e.expected_display}, got ${e.actual}`)
|
|
89
|
-
.join('; '));
|
|
90
|
-
}
|
|
91
|
-
if (unknownKeys.length)
|
|
92
|
-
parts.push(`unknown keys: ${unknownKeys.join(', ')}`);
|
|
93
|
-
const message = `Workflow input validation failed: ${parts.join(' | ')}`;
|
|
94
|
-
throw new InputValidationError(message, missingRequired, invalidTypes, unknownKeys);
|
|
95
|
-
}
|
|
34
|
+
validateInputSchema(input_schema ?? {}, payload);
|
|
96
35
|
}
|
|
97
36
|
}
|
|
@@ -15,27 +15,43 @@ export interface Workflow {
|
|
|
15
15
|
enable_service_unavailable_recovery: boolean;
|
|
16
16
|
enable_action_timing_recovery: boolean;
|
|
17
17
|
}
|
|
18
|
-
export type WorkflowPropertySchema = string | string[] | {
|
|
18
|
+
export type WorkflowPropertySchema = boolean | string | string[] | {
|
|
19
19
|
type?: string | string[];
|
|
20
20
|
[key: string]: unknown;
|
|
21
21
|
};
|
|
22
22
|
export interface WorkflowInputSchema {
|
|
23
|
-
type?:
|
|
23
|
+
type?: string | string[];
|
|
24
24
|
properties?: Record<string, WorkflowPropertySchema>;
|
|
25
25
|
required?: string[];
|
|
26
|
-
additionalProperties?:
|
|
26
|
+
additionalProperties?: WorkflowPropertySchema;
|
|
27
|
+
[key: string]: unknown;
|
|
27
28
|
}
|
|
29
|
+
export interface WorkflowVaultCredentialSchema {
|
|
30
|
+
type: 'credential';
|
|
31
|
+
domain: string;
|
|
32
|
+
example?: string;
|
|
33
|
+
}
|
|
34
|
+
export type WorkflowVaultSchema = Record<string, WorkflowVaultCredentialSchema>;
|
|
28
35
|
export interface WorkflowMetadata {
|
|
29
36
|
input_schema: WorkflowInputSchema;
|
|
37
|
+
workspace_id?: string;
|
|
38
|
+
vault_schema?: WorkflowVaultSchema;
|
|
30
39
|
}
|
|
31
40
|
export interface InvalidTypeDetail {
|
|
32
41
|
field: string;
|
|
33
42
|
expected_display: string;
|
|
34
43
|
actual: string;
|
|
35
44
|
}
|
|
45
|
+
export interface SchemaErrorDetail {
|
|
46
|
+
instancePath: string;
|
|
47
|
+
schemaPath: string;
|
|
48
|
+
keyword: string;
|
|
49
|
+
message: string;
|
|
50
|
+
}
|
|
36
51
|
export declare class InputValidationError extends Error {
|
|
37
52
|
readonly missingRequired: string[];
|
|
38
53
|
readonly invalidTypes: InvalidTypeDetail[];
|
|
39
54
|
readonly unknownKeys: string[];
|
|
40
|
-
|
|
55
|
+
readonly schemaErrors: SchemaErrorDetail[];
|
|
56
|
+
constructor(message?: string, missingRequired?: string[], invalidTypes?: InvalidTypeDetail[], unknownKeys?: string[], schemaErrors?: SchemaErrorDetail[]);
|
|
41
57
|
}
|
package/dist/workflows/types.js
CHANGED
|
@@ -5,11 +5,13 @@ export class InputValidationError extends Error {
|
|
|
5
5
|
missingRequired;
|
|
6
6
|
invalidTypes;
|
|
7
7
|
unknownKeys;
|
|
8
|
-
|
|
8
|
+
schemaErrors;
|
|
9
|
+
constructor(message = 'Input validation failed', missingRequired = [], invalidTypes = [], unknownKeys = [], schemaErrors = []) {
|
|
9
10
|
super(message);
|
|
10
11
|
this.name = 'InputValidationError';
|
|
11
12
|
this.missingRequired = missingRequired;
|
|
12
13
|
this.invalidTypes = invalidTypes;
|
|
13
14
|
this.unknownKeys = unknownKeys;
|
|
15
|
+
this.schemaErrors = schemaErrors;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { InputValidationError, type WorkflowInputSchema } from './types.js';
|
|
2
|
+
export declare function validateInputSchema(schema: WorkflowInputSchema, payload: Record<string, unknown>): void;
|
|
3
|
+
export declare function inputValidationErrorFromResponse(response: Record<string, unknown>, requestBody?: unknown): InputValidationError | undefined;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import { InputValidationError } from './types.js';
|
|
3
|
+
const ajv = new Ajv({
|
|
4
|
+
allErrors: true,
|
|
5
|
+
strict: true,
|
|
6
|
+
validateFormats: false,
|
|
7
|
+
addUsedSchema: false,
|
|
8
|
+
removeAdditional: false,
|
|
9
|
+
strictSchema: false,
|
|
10
|
+
keywords: ['example']
|
|
11
|
+
});
|
|
12
|
+
const singleSchemaKeywords = new Set([
|
|
13
|
+
'additionalItems',
|
|
14
|
+
'additionalProperties',
|
|
15
|
+
'contains',
|
|
16
|
+
'else',
|
|
17
|
+
'if',
|
|
18
|
+
'not',
|
|
19
|
+
'propertyNames',
|
|
20
|
+
'then'
|
|
21
|
+
]);
|
|
22
|
+
const arraySchemaKeywords = new Set(['allOf', 'anyOf', 'oneOf']);
|
|
23
|
+
const mappingSchemaKeywords = new Set([
|
|
24
|
+
'definitions',
|
|
25
|
+
'patternProperties',
|
|
26
|
+
'properties'
|
|
27
|
+
]);
|
|
28
|
+
export function validateInputSchema(schema, payload) {
|
|
29
|
+
const externalRef = firstExternalRef(schema);
|
|
30
|
+
if (externalRef) {
|
|
31
|
+
const detail = {
|
|
32
|
+
instancePath: '',
|
|
33
|
+
schemaPath: externalRef.path,
|
|
34
|
+
keyword: '$ref',
|
|
35
|
+
message: `external reference is not allowed: ${externalRef.ref}`
|
|
36
|
+
};
|
|
37
|
+
throw new InputValidationError(`Workflow input schema is invalid: ${detail.message}`, [], [], [], [detail]);
|
|
38
|
+
}
|
|
39
|
+
let validate;
|
|
40
|
+
try {
|
|
41
|
+
validate = ajv.compile(schema);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
const detail = {
|
|
46
|
+
instancePath: '',
|
|
47
|
+
schemaPath: '#',
|
|
48
|
+
keyword: 'schema',
|
|
49
|
+
message
|
|
50
|
+
};
|
|
51
|
+
throw new InputValidationError(`Workflow input schema is invalid: ${message}`, [], [], [], [detail]);
|
|
52
|
+
}
|
|
53
|
+
if (validate(payload))
|
|
54
|
+
return;
|
|
55
|
+
const errors = [...(validate.errors ?? [])].sort(compareErrors);
|
|
56
|
+
const schemaErrors = errors.map(toSchemaError);
|
|
57
|
+
const missingRequired = missingRequiredFields(errors);
|
|
58
|
+
const invalidTypes = invalidTypeDetails(errors, payload);
|
|
59
|
+
const unknownKeys = unknownPropertyNames(errors);
|
|
60
|
+
throw new InputValidationError(validationMessage(schemaErrors, missingRequired, invalidTypes, unknownKeys), missingRequired, invalidTypes, unknownKeys, schemaErrors);
|
|
61
|
+
}
|
|
62
|
+
export function inputValidationErrorFromResponse(response, requestBody) {
|
|
63
|
+
const rawErrors = response.run_input_variables_errors;
|
|
64
|
+
if (!Array.isArray(rawErrors))
|
|
65
|
+
return undefined;
|
|
66
|
+
const schema = isObject(response.input_schema) ? response.input_schema : {};
|
|
67
|
+
const payload = isObject(requestBody) && isObject(requestBody.run_input_variables)
|
|
68
|
+
? requestBody.run_input_variables
|
|
69
|
+
: {};
|
|
70
|
+
const schemaErrors = [];
|
|
71
|
+
const missingRequired = [];
|
|
72
|
+
const invalidTypes = [];
|
|
73
|
+
const unknownKeys = [];
|
|
74
|
+
for (const rawError of rawErrors) {
|
|
75
|
+
if (!isObject(rawError))
|
|
76
|
+
continue;
|
|
77
|
+
const field = String(rawError.field ?? '');
|
|
78
|
+
const keyword = String(rawError.keyword ?? 'validation');
|
|
79
|
+
const message = String(rawError.message ?? 'Invalid value');
|
|
80
|
+
const expected = isObject(rawError.expected) ? rawError.expected : {};
|
|
81
|
+
const instancePath = field.startsWith('/') ? field : '';
|
|
82
|
+
const schemaPath = field.startsWith('#')
|
|
83
|
+
? field
|
|
84
|
+
: schemaPathForInstance(schema, instancePath, keyword);
|
|
85
|
+
schemaErrors.push({ instancePath, schemaPath, keyword, message });
|
|
86
|
+
if (keyword === 'required') {
|
|
87
|
+
const missingProperty = expected.missingProperty;
|
|
88
|
+
if (typeof missingProperty === 'string') {
|
|
89
|
+
const missingField = instancePath
|
|
90
|
+
? `${instancePath}/${escapePointer(missingProperty)}`
|
|
91
|
+
: missingProperty;
|
|
92
|
+
pushUnique(missingRequired, missingField);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (keyword === 'type') {
|
|
96
|
+
const expectedType = expected.type;
|
|
97
|
+
const expectedTypes = Array.isArray(expectedType)
|
|
98
|
+
? expectedType.map(String)
|
|
99
|
+
: String(expectedType ?? 'any').split(',');
|
|
100
|
+
invalidTypes.push({
|
|
101
|
+
field: displayField(instancePath),
|
|
102
|
+
expected_display: expectedTypes.join(' | '),
|
|
103
|
+
actual: detectType(valueAtPointer(payload, instancePath))
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else if (keyword === 'additionalProperties') {
|
|
107
|
+
const additionalProperty = expected.additionalProperty;
|
|
108
|
+
if (typeof additionalProperty === 'string') {
|
|
109
|
+
const unknownField = instancePath
|
|
110
|
+
? `${instancePath}/${escapePointer(additionalProperty)}`
|
|
111
|
+
: additionalProperty;
|
|
112
|
+
pushUnique(unknownKeys, unknownField);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!schemaErrors.length) {
|
|
117
|
+
schemaErrors.push({
|
|
118
|
+
instancePath: '',
|
|
119
|
+
schemaPath: '',
|
|
120
|
+
keyword: 'validation',
|
|
121
|
+
message: String(response.message ?? 'Validation failed for run_input_variables')
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return new InputValidationError(validationMessage(schemaErrors, missingRequired, invalidTypes, unknownKeys), missingRequired, invalidTypes, unknownKeys, schemaErrors);
|
|
125
|
+
}
|
|
126
|
+
function firstExternalRef(schema, path = []) {
|
|
127
|
+
if (!isObject(schema))
|
|
128
|
+
return undefined;
|
|
129
|
+
if (typeof schema.$ref === 'string' && !schema.$ref.startsWith('#')) {
|
|
130
|
+
return {
|
|
131
|
+
path: jsonPointer([...path, '$ref'], '#'),
|
|
132
|
+
ref: schema.$ref
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
for (const keyword of singleSchemaKeywords) {
|
|
136
|
+
const found = firstExternalRef(schema[keyword], [...path, keyword]);
|
|
137
|
+
if (found)
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
const items = schema.items;
|
|
141
|
+
if (Array.isArray(items)) {
|
|
142
|
+
for (const [index, child] of items.entries()) {
|
|
143
|
+
const found = firstExternalRef(child, [...path, 'items', index]);
|
|
144
|
+
if (found)
|
|
145
|
+
return found;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const found = firstExternalRef(items, [...path, 'items']);
|
|
150
|
+
if (found)
|
|
151
|
+
return found;
|
|
152
|
+
}
|
|
153
|
+
for (const keyword of arraySchemaKeywords) {
|
|
154
|
+
const children = schema[keyword];
|
|
155
|
+
if (!Array.isArray(children))
|
|
156
|
+
continue;
|
|
157
|
+
for (const [index, child] of children.entries()) {
|
|
158
|
+
const found = firstExternalRef(child, [...path, keyword, index]);
|
|
159
|
+
if (found)
|
|
160
|
+
return found;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const keyword of mappingSchemaKeywords) {
|
|
164
|
+
const children = schema[keyword];
|
|
165
|
+
if (!isObject(children))
|
|
166
|
+
continue;
|
|
167
|
+
for (const [name, child] of Object.entries(children)) {
|
|
168
|
+
const found = firstExternalRef(child, [...path, keyword, name]);
|
|
169
|
+
if (found)
|
|
170
|
+
return found;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const dependencies = schema.dependencies;
|
|
174
|
+
if (isObject(dependencies)) {
|
|
175
|
+
for (const [name, child] of Object.entries(dependencies)) {
|
|
176
|
+
if (!isObject(child))
|
|
177
|
+
continue;
|
|
178
|
+
const found = firstExternalRef(child, [...path, 'dependencies', name]);
|
|
179
|
+
if (found)
|
|
180
|
+
return found;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
function compareErrors(left, right) {
|
|
186
|
+
return (left.instancePath.localeCompare(right.instancePath) ||
|
|
187
|
+
left.schemaPath.localeCompare(right.schemaPath) ||
|
|
188
|
+
left.keyword.localeCompare(right.keyword) ||
|
|
189
|
+
(left.message ?? '').localeCompare(right.message ?? ''));
|
|
190
|
+
}
|
|
191
|
+
function toSchemaError(error) {
|
|
192
|
+
return {
|
|
193
|
+
instancePath: error.instancePath,
|
|
194
|
+
schemaPath: error.schemaPath,
|
|
195
|
+
keyword: error.keyword,
|
|
196
|
+
message: error.message ?? 'validation failed'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function missingRequiredFields(errors) {
|
|
200
|
+
const fields = [];
|
|
201
|
+
for (const error of errors) {
|
|
202
|
+
if (error.keyword !== 'required')
|
|
203
|
+
continue;
|
|
204
|
+
const missingProperty = String(error.params.missingProperty);
|
|
205
|
+
const field = error.instancePath
|
|
206
|
+
? `${error.instancePath}/${escapePointer(missingProperty)}`
|
|
207
|
+
: missingProperty;
|
|
208
|
+
pushUnique(fields, field);
|
|
209
|
+
}
|
|
210
|
+
return fields;
|
|
211
|
+
}
|
|
212
|
+
function invalidTypeDetails(errors, payload) {
|
|
213
|
+
const details = [];
|
|
214
|
+
for (const error of errors) {
|
|
215
|
+
if (error.keyword !== 'type')
|
|
216
|
+
continue;
|
|
217
|
+
const expected = String(error.params.type).split(',').join(' | ');
|
|
218
|
+
const value = valueAtPointer(payload, error.instancePath);
|
|
219
|
+
details.push({
|
|
220
|
+
field: displayField(error.instancePath),
|
|
221
|
+
expected_display: expected,
|
|
222
|
+
actual: detectType(value)
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return details;
|
|
226
|
+
}
|
|
227
|
+
function unknownPropertyNames(errors) {
|
|
228
|
+
const fields = [];
|
|
229
|
+
for (const error of errors) {
|
|
230
|
+
if (error.keyword !== 'additionalProperties')
|
|
231
|
+
continue;
|
|
232
|
+
const property = String(error.params.additionalProperty);
|
|
233
|
+
const field = error.instancePath
|
|
234
|
+
? `${error.instancePath}/${escapePointer(property)}`
|
|
235
|
+
: property;
|
|
236
|
+
pushUnique(fields, field);
|
|
237
|
+
}
|
|
238
|
+
return fields;
|
|
239
|
+
}
|
|
240
|
+
function validationMessage(schemaErrors, missingRequired, invalidTypes, unknownKeys) {
|
|
241
|
+
const parts = [];
|
|
242
|
+
if (missingRequired.length) {
|
|
243
|
+
parts.push(`missing required: ${missingRequired.join(', ')}`);
|
|
244
|
+
}
|
|
245
|
+
if (invalidTypes.length) {
|
|
246
|
+
parts.push(invalidTypes
|
|
247
|
+
.map((error) => `${error.field}: expected ${error.expected_display}, got ${error.actual}`)
|
|
248
|
+
.join('; '));
|
|
249
|
+
}
|
|
250
|
+
if (unknownKeys.length) {
|
|
251
|
+
parts.push(`unknown keys: ${unknownKeys.join(', ')}`);
|
|
252
|
+
}
|
|
253
|
+
const extraErrors = schemaErrors.filter((error) => !['required', 'type', 'additionalProperties'].includes(error.keyword));
|
|
254
|
+
if (extraErrors.length) {
|
|
255
|
+
parts.push(extraErrors
|
|
256
|
+
.map((error) => `${error.instancePath || '/'}: ${error.message}`)
|
|
257
|
+
.join('; '));
|
|
258
|
+
}
|
|
259
|
+
return `Workflow input validation failed: ${parts.join(' | ')}`;
|
|
260
|
+
}
|
|
261
|
+
function valueAtPointer(value, pointer) {
|
|
262
|
+
if (!pointer)
|
|
263
|
+
return value;
|
|
264
|
+
return pointer
|
|
265
|
+
.slice(1)
|
|
266
|
+
.split('/')
|
|
267
|
+
.map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
268
|
+
.reduce((current, part) => {
|
|
269
|
+
if (Array.isArray(current))
|
|
270
|
+
return current[Number(part)];
|
|
271
|
+
if (isObject(current))
|
|
272
|
+
return current[part];
|
|
273
|
+
return undefined;
|
|
274
|
+
}, value);
|
|
275
|
+
}
|
|
276
|
+
function displayField(instancePath) {
|
|
277
|
+
if (!instancePath)
|
|
278
|
+
return '$';
|
|
279
|
+
const withoutPrefix = instancePath.slice(1);
|
|
280
|
+
return withoutPrefix.includes('/') ? instancePath : withoutPrefix;
|
|
281
|
+
}
|
|
282
|
+
function schemaPathForInstance(schema, instancePath, keyword) {
|
|
283
|
+
let current = schema;
|
|
284
|
+
const schemaPath = [];
|
|
285
|
+
const parts = instancePath
|
|
286
|
+
? instancePath
|
|
287
|
+
.slice(1)
|
|
288
|
+
.split('/')
|
|
289
|
+
.map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
290
|
+
: [];
|
|
291
|
+
for (const part of parts) {
|
|
292
|
+
if (!isObject(current))
|
|
293
|
+
return '';
|
|
294
|
+
const properties = current.properties;
|
|
295
|
+
if (isObject(properties) && part in properties) {
|
|
296
|
+
current = properties[part];
|
|
297
|
+
schemaPath.push('properties', part);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const items = current.items;
|
|
301
|
+
if (isObject(items) && /^\d+$/.test(part)) {
|
|
302
|
+
current = items;
|
|
303
|
+
schemaPath.push('items');
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const patternProperties = current.patternProperties;
|
|
307
|
+
if (isObject(patternProperties)) {
|
|
308
|
+
const match = Object.entries(patternProperties).find(([pattern]) => {
|
|
309
|
+
try {
|
|
310
|
+
return new RegExp(pattern).test(part);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
if (match) {
|
|
317
|
+
current = match[1];
|
|
318
|
+
schemaPath.push('patternProperties', match[0]);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return '';
|
|
323
|
+
}
|
|
324
|
+
if (isObject(current) && keyword in current) {
|
|
325
|
+
schemaPath.push(keyword);
|
|
326
|
+
return jsonPointer(schemaPath, '#');
|
|
327
|
+
}
|
|
328
|
+
return '';
|
|
329
|
+
}
|
|
330
|
+
function detectType(value) {
|
|
331
|
+
if (value === null)
|
|
332
|
+
return 'null';
|
|
333
|
+
if (Array.isArray(value))
|
|
334
|
+
return 'array';
|
|
335
|
+
if (typeof value === 'number') {
|
|
336
|
+
return Number.isInteger(value) ? 'integer' : 'number';
|
|
337
|
+
}
|
|
338
|
+
return typeof value;
|
|
339
|
+
}
|
|
340
|
+
function jsonPointer(path, prefix = '') {
|
|
341
|
+
if (!path.length)
|
|
342
|
+
return prefix;
|
|
343
|
+
return `${prefix}/${path.map((part) => escapePointer(String(part))).join('/')}`;
|
|
344
|
+
}
|
|
345
|
+
function escapePointer(value) {
|
|
346
|
+
return value.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
347
|
+
}
|
|
348
|
+
function pushUnique(values, value) {
|
|
349
|
+
if (!values.includes(value))
|
|
350
|
+
values.push(value);
|
|
351
|
+
}
|
|
352
|
+
function isObject(value) {
|
|
353
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
354
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudcruise",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "The official CloudCruise JS/TS client.",
|
|
5
5
|
"homepage": "https://github.com/CloudCruise/cloudcruise-js#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"ajv": "^8.20.0"
|
|
33
36
|
}
|
|
34
37
|
}
|