cloudcruise 0.0.1 → 0.0.2-alpha.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.
@@ -0,0 +1,99 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
2
+ /**
3
+ * AES-256-GCM Encryption utilities for CloudCruise vault data
4
+ * Uses 12-byte IV and returns concatenated hex: iv(24 hex) + ciphertext + tag(32 hex)
5
+ */
6
+ /**
7
+ * Encrypts sensitive data using AES-256-GCM
8
+ * @param data - Data to encrypt (will be JSON stringified)
9
+ * @param keyHex - Hex-encoded encryption key
10
+ * @returns Concatenated hex string: iv(24 hex) + ciphertext + tag(32 hex)
11
+ */
12
+ export async function encryptData(data, keyHex) {
13
+ try {
14
+ const key = Buffer.from(keyHex, 'hex');
15
+ const iv = randomBytes(12); // 12-byte IV for GCM
16
+ const jsonData = JSON.stringify(data);
17
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
18
+ let encrypted = cipher.update(jsonData, 'utf8', 'hex');
19
+ encrypted += cipher.final('hex');
20
+ const tag = cipher.getAuthTag().toString('hex');
21
+ return iv.toString('hex') + encrypted + tag;
22
+ }
23
+ catch (error) {
24
+ throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
25
+ }
26
+ }
27
+ /**
28
+ * Decrypts data using AES-256-GCM
29
+ * @param encryptedHex - Concatenated hex: iv(24 hex) + ciphertext + tag(32 hex)
30
+ * @param keyHex - Hex-encoded encryption key
31
+ * @returns Decrypted and parsed data
32
+ */
33
+ export async function decryptData(encryptedHex, keyHex) {
34
+ try {
35
+ if (typeof encryptedHex !== 'string' || encryptedHex.length < 56) {
36
+ throw new Error('Invalid encrypted payload');
37
+ }
38
+ const key = Buffer.from(keyHex, 'hex');
39
+ const iv = Buffer.from(encryptedHex.slice(0, 24), 'hex');
40
+ const tag = Buffer.from(encryptedHex.slice(-32), 'hex');
41
+ const encrypted = encryptedHex.slice(24, -32);
42
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
43
+ decipher.setAuthTag(tag);
44
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
45
+ decrypted += decipher.final('utf8');
46
+ return JSON.parse(decrypted);
47
+ }
48
+ catch (error) {
49
+ throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
50
+ }
51
+ }
52
+ /**
53
+ * Encrypts sensitive fields in a vault entry
54
+ * Fields encrypted: user_name, password, tfa_secret (if present)
55
+ * @param entry - Vault entry with potentially sensitive data
56
+ * @param encryptionKey - Hex-encoded encryption key
57
+ * @returns Entry with encrypted sensitive fields
58
+ */
59
+ export async function encryptSensitiveFields(entry, encryptionKey) {
60
+ const encryptedEntry = { ...entry };
61
+ if (entry.user_name !== undefined) {
62
+ encryptedEntry.user_name = await encryptData(entry.user_name, encryptionKey);
63
+ }
64
+ if (entry.password !== undefined) {
65
+ encryptedEntry.password = await encryptData(entry.password, encryptionKey);
66
+ }
67
+ if (entry.tfa_secret !== undefined) {
68
+ encryptedEntry.tfa_secret = await encryptData(entry.tfa_secret, encryptionKey);
69
+ }
70
+ return encryptedEntry;
71
+ }
72
+ /**
73
+ * Decrypts sensitive fields in a vault entry
74
+ * @param entry - Vault entry with encrypted sensitive fields
75
+ * @param encryptionKey - Hex-encoded encryption key
76
+ * @returns Entry with decrypted sensitive fields
77
+ */
78
+ export async function decryptSensitiveFields(entry, encryptionKey) {
79
+ const decryptedEntry = { ...entry };
80
+ if (typeof entry.user_name === 'string') {
81
+ try {
82
+ decryptedEntry.user_name = await decryptData(entry.user_name, encryptionKey);
83
+ }
84
+ catch { }
85
+ }
86
+ if (typeof entry.password === 'string') {
87
+ try {
88
+ decryptedEntry.password = await decryptData(entry.password, encryptionKey);
89
+ }
90
+ catch { }
91
+ }
92
+ if (typeof entry.tfa_secret === 'string') {
93
+ try {
94
+ decryptedEntry.tfa_secret = await decryptData(entry.tfa_secret, encryptionKey);
95
+ }
96
+ catch { }
97
+ }
98
+ return decryptedEntry;
99
+ }
@@ -0,0 +1,5 @@
1
+ import type { WebhookPayload, WebhookVerificationOptions } from './types.js';
2
+ export declare class WebhookClient {
3
+ constructor();
4
+ verifySignature(receivedData: any, receivedSignature: string, secretKey: string, options?: WebhookVerificationOptions): WebhookPayload;
5
+ }
@@ -0,0 +1,14 @@
1
+ import { verifyMessage } from './utils.js';
2
+ export class WebhookClient {
3
+ constructor() {
4
+ // No makeRequest needed for webhook verification
5
+ }
6
+ /*
7
+ 1. receivedSignature will be in the request header: "x-hmac-signature"
8
+ 2. receivedData will be the request body.
9
+ 3. secretKey is the key you set when creating this webhook in the CloudCruise portal.
10
+ */
11
+ verifySignature(receivedData, receivedSignature, secretKey, options) {
12
+ return verifyMessage(receivedData, receivedSignature, secretKey, options);
13
+ }
14
+ }
@@ -0,0 +1,13 @@
1
+ import { EventType } from "../runs/types";
2
+ export declare class VerificationError extends Error {
3
+ readonly statusCode: number;
4
+ constructor(message?: string, statusCode?: number);
5
+ }
6
+ export interface WebhookPayload {
7
+ event: EventType;
8
+ expires_at: number;
9
+ [key: string]: any;
10
+ }
11
+ export interface WebhookVerificationOptions {
12
+ allowExpired?: boolean;
13
+ }
@@ -0,0 +1,8 @@
1
+ export class VerificationError extends Error {
2
+ statusCode;
3
+ constructor(message = "Verification failed", statusCode = 400) {
4
+ super(message);
5
+ this.statusCode = statusCode;
6
+ this.name = "VerificationError";
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ import { type WebhookPayload, type WebhookVerificationOptions } from './types.js';
2
+ export declare function verifyMessage(receivedData: any, receivedSignature: string, secretKey: string, options?: WebhookVerificationOptions): WebhookPayload;
@@ -0,0 +1,49 @@
1
+ import crypto from 'crypto';
2
+ import { VerificationError } from './types.js';
3
+ function verifyHmac(receivedData, receivedSignature, secretKey) {
4
+ const hmac = crypto.createHmac("sha256", secretKey);
5
+ hmac.update(receivedData);
6
+ const calculatedSignature = hmac.digest("hex");
7
+ const formattedReceivedSignature = receivedSignature.split("=")[1];
8
+ if (formattedReceivedSignature.length !== calculatedSignature.length) {
9
+ return false;
10
+ }
11
+ return crypto.timingSafeEqual(Buffer.from(calculatedSignature, "hex"), Buffer.from(formattedReceivedSignature, "hex"));
12
+ }
13
+ export function verifyMessage(receivedData, receivedSignature, secretKey, options) {
14
+ if (!receivedData) {
15
+ throw new VerificationError("Received request without body", 400);
16
+ }
17
+ if (!receivedSignature) {
18
+ throw new VerificationError("Missing HMAC signature", 400);
19
+ }
20
+ if (!secretKey) {
21
+ throw new VerificationError("Missing secret key", 400);
22
+ }
23
+ let dataJson;
24
+ let dataString;
25
+ if (typeof receivedData === 'string') {
26
+ dataString = receivedData;
27
+ try {
28
+ dataJson = JSON.parse(receivedData);
29
+ }
30
+ catch (error) {
31
+ throw new VerificationError(`Failed to decode JSON: ${error instanceof Error ? error.message : 'Unknown error'}`, 400);
32
+ }
33
+ }
34
+ else {
35
+ dataJson = receivedData;
36
+ dataString = JSON.stringify(receivedData);
37
+ }
38
+ const expiresAt = dataJson.expires_at;
39
+ if (!expiresAt) {
40
+ throw new VerificationError("No expiration date sent", 400);
41
+ }
42
+ if (!verifyHmac(dataString, receivedSignature, secretKey)) {
43
+ throw new VerificationError("Invalid HMAC signature", 401);
44
+ }
45
+ if (!options?.allowExpired && Date.now() / 1000 > expiresAt) {
46
+ throw new VerificationError("Webhook message expired", 400);
47
+ }
48
+ return dataJson;
49
+ }
@@ -0,0 +1,19 @@
1
+ import type { Workflow, WorkflowMetadata } from './types.js';
2
+ export declare class WorkflowsClient {
3
+ private readonly makeRequest;
4
+ constructor(makeRequest: <T = any>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: any) => Promise<T>);
5
+ /**
6
+ * Retrieves all workflows of the workspace the API key is associated with
7
+ */
8
+ getAllWorkflows(): Promise<Workflow[]>;
9
+ /**
10
+ * Retrieves the JSON schema of the input variables for a specific workflow
11
+ * @param workflowId - The ID of the workflow
12
+ */
13
+ getWorkflowMetadata(workflowId: string): Promise<WorkflowMetadata>;
14
+ /**
15
+ * Validates a payload against a workflow's input schema.
16
+ * Throws InputValidationError if invalid; resolves if valid.
17
+ */
18
+ validateWorkflowInput(workflowId: string, payload: Record<string, any>): Promise<void>;
19
+ }
@@ -0,0 +1,97 @@
1
+ import { InputValidationError } from './types.js';
2
+ export class WorkflowsClient {
3
+ makeRequest;
4
+ constructor(makeRequest) {
5
+ this.makeRequest = makeRequest;
6
+ }
7
+ /**
8
+ * Retrieves all workflows of the workspace the API key is associated with
9
+ */
10
+ async getAllWorkflows() {
11
+ return await this.makeRequest('GET', '/workflows');
12
+ }
13
+ /**
14
+ * Retrieves the JSON schema of the input variables for a specific workflow
15
+ * @param workflowId - The ID of the workflow
16
+ */
17
+ async getWorkflowMetadata(workflowId) {
18
+ const path = `/workflows/${workflowId}/metadata`;
19
+ return await this.makeRequest('GET', path);
20
+ }
21
+ /**
22
+ * Validates a payload against a workflow's input schema.
23
+ * Throws InputValidationError if invalid; resolves if valid.
24
+ */
25
+ async validateWorkflowInput(workflowId, payload) {
26
+ const { input_schema } = await this.getWorkflowMetadata(workflowId);
27
+ const schema = input_schema ?? {};
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
+ }
96
+ }
97
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * CloudCruise Workflows API Type Definitions
3
+ */
4
+ export interface Workflow {
5
+ id: string;
6
+ name: string;
7
+ description?: string | null;
8
+ created_at: string;
9
+ updated_at: string;
10
+ workspace_id: string;
11
+ created_by: string;
12
+ enable_popup_handling: boolean;
13
+ enable_xpath_recovery: boolean;
14
+ enable_error_code_generation: boolean;
15
+ enable_service_unavailable_recovery: boolean;
16
+ enable_action_timing_recovery: boolean;
17
+ }
18
+ export type WorkflowPropertySchema = string | string[] | {
19
+ type?: string | string[];
20
+ [key: string]: unknown;
21
+ };
22
+ export interface WorkflowInputSchema {
23
+ type?: 'object';
24
+ properties?: Record<string, WorkflowPropertySchema>;
25
+ required?: string[];
26
+ additionalProperties?: boolean;
27
+ }
28
+ export interface WorkflowMetadata {
29
+ input_schema: WorkflowInputSchema;
30
+ }
31
+ export interface InvalidTypeDetail {
32
+ field: string;
33
+ expected_display: string;
34
+ actual: string;
35
+ }
36
+ export declare class InputValidationError extends Error {
37
+ readonly missingRequired: string[];
38
+ readonly invalidTypes: InvalidTypeDetail[];
39
+ readonly unknownKeys: string[];
40
+ constructor(message?: string, missingRequired?: string[], invalidTypes?: InvalidTypeDetail[], unknownKeys?: string[]);
41
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CloudCruise Workflows API Type Definitions
3
+ */
4
+ export class InputValidationError extends Error {
5
+ missingRequired;
6
+ invalidTypes;
7
+ unknownKeys;
8
+ constructor(message = 'Input validation failed', missingRequired = [], invalidTypes = [], unknownKeys = []) {
9
+ super(message);
10
+ this.name = 'InputValidationError';
11
+ this.missingRequired = missingRequired;
12
+ this.invalidTypes = invalidTypes;
13
+ this.unknownKeys = unknownKeys;
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudcruise",
3
- "version": "0.0.1",
3
+ "version": "0.0.2-alpha.1",
4
4
  "description": "The official CloudCruise JS/TS client.",
5
5
  "homepage": "https://github.com/CloudCruise/cloudcruise-js#readme",
6
6
  "bugs": {
@@ -13,8 +13,21 @@
13
13
  "license": "MIT",
14
14
  "author": "CloudCruise",
15
15
  "type": "module",
16
- "main": "index.js",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "files": [
19
+ "dist"
20
+ ],
17
21
  "scripts": {
22
+ "build": "tsc",
23
+ "dev": "tsc --watch",
18
24
  "test": "echo \"Error: no test specified\" && exit 1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.0.0",
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
19
32
  }
20
33
  }