@qaick/http-executor 0.1.0-alpha.5 → 0.1.0-alpha.7
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.
|
@@ -2,7 +2,7 @@ import type { ActionExecutionContext, ActionExecutionResult, ActionExecutor } fr
|
|
|
2
2
|
export interface HttpResponse {
|
|
3
3
|
status: number;
|
|
4
4
|
body: unknown;
|
|
5
|
-
headers?: Record<string, string>;
|
|
5
|
+
headers?: Record<string, string | string[]>;
|
|
6
6
|
}
|
|
7
7
|
export interface HttpTransport {
|
|
8
8
|
request(url: string, init: {
|
|
@@ -13,39 +13,6 @@ export interface HttpTransport {
|
|
|
13
13
|
timeoutMs?: number;
|
|
14
14
|
}): Promise<HttpResponse>;
|
|
15
15
|
}
|
|
16
|
-
/** Host-owned cookies that already exist before a QAick execution starts. */
|
|
17
|
-
export type HttpAmbientCookieProvider = (url: string) => Promise<string | undefined>;
|
|
18
|
-
/** Internal Runtime-to-HTTP execution key. It is not part of persisted Action data. */
|
|
19
|
-
export declare const HTTP_EXECUTION_ID_KEY = "__qaick_execution_id";
|
|
20
|
-
export interface HttpCookie {
|
|
21
|
-
name: string;
|
|
22
|
-
value: string;
|
|
23
|
-
domain: string;
|
|
24
|
-
path: string;
|
|
25
|
-
hostOnly: boolean;
|
|
26
|
-
secure: boolean;
|
|
27
|
-
expiresAt?: number;
|
|
28
|
-
}
|
|
29
|
-
/** Cookie state shared by HTTP Actions during one execution. */
|
|
30
|
-
export declare class HttpCookieJar {
|
|
31
|
-
private readonly cookies;
|
|
32
|
-
store(url: string, setCookie: string | string[] | undefined): void;
|
|
33
|
-
header(url: string): string | undefined;
|
|
34
|
-
}
|
|
35
|
-
/** HTTP-specific session boundary. One instance must be created per QAick execution. */
|
|
36
|
-
export declare class HttpExecutionSession implements HttpTransport {
|
|
37
|
-
private readonly transport;
|
|
38
|
-
readonly cookies: HttpCookieJar;
|
|
39
|
-
private readonly ambientCookies?;
|
|
40
|
-
constructor(transport: HttpTransport, cookies?: HttpCookieJar, ambientCookies?: HttpAmbientCookieProvider | undefined);
|
|
41
|
-
request(url: string, init: {
|
|
42
|
-
method: string;
|
|
43
|
-
body?: unknown;
|
|
44
|
-
headers?: Record<string, string>;
|
|
45
|
-
signal: AbortSignal;
|
|
46
|
-
timeoutMs?: number;
|
|
47
|
-
}): Promise<HttpResponse>;
|
|
48
|
-
}
|
|
49
16
|
export type HttpActionErrorCode = 'HTTP_INVALID_URL' | 'HTTP_REQUEST_CONSTRUCTION_ERROR' | 'HTTP_ABORTED' | 'HTTP_TIMEOUT' | 'HTTP_NETWORK_ERROR' | 'HTTP_RESPONSE_PARSE_ERROR' | 'HTTP_STATUS_ERROR';
|
|
50
17
|
export declare class HttpActionError extends Error {
|
|
51
18
|
readonly code: HttpActionErrorCode;
|
|
@@ -63,9 +30,7 @@ export declare class FetchHttpTransport implements HttpTransport {
|
|
|
63
30
|
}
|
|
64
31
|
export declare class HttpActionExecutor implements ActionExecutor {
|
|
65
32
|
private readonly transport;
|
|
66
|
-
private readonly sessions;
|
|
67
33
|
constructor(transport: HttpTransport);
|
|
68
34
|
supports(action: string): boolean;
|
|
69
35
|
execute(context: ActionExecutionContext): Promise<ActionExecutionResult>;
|
|
70
|
-
private sessionFor;
|
|
71
36
|
}
|
|
@@ -1,79 +1,3 @@
|
|
|
1
|
-
/** Internal Runtime-to-HTTP execution key. It is not part of persisted Action data. */
|
|
2
|
-
export const HTTP_EXECUTION_ID_KEY = '__qaick_execution_id';
|
|
3
|
-
/** Cookie state shared by HTTP Actions during one execution. */
|
|
4
|
-
export class HttpCookieJar {
|
|
5
|
-
cookies = new Map();
|
|
6
|
-
store(url, setCookie) {
|
|
7
|
-
if (!setCookie)
|
|
8
|
-
return;
|
|
9
|
-
const parsedUrl = new URL(url);
|
|
10
|
-
for (const header of (Array.isArray(setCookie) ? setCookie : splitSetCookieHeader(setCookie))) {
|
|
11
|
-
const cookie = parseSetCookie(header, parsedUrl);
|
|
12
|
-
if (!cookie)
|
|
13
|
-
continue;
|
|
14
|
-
const key = cookieKey(cookie);
|
|
15
|
-
if (cookie.expiresAt !== undefined && cookie.expiresAt <= Date.now())
|
|
16
|
-
this.cookies.delete(key);
|
|
17
|
-
else
|
|
18
|
-
this.cookies.set(key, cookie);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
header(url) {
|
|
22
|
-
const parsedUrl = new URL(url);
|
|
23
|
-
const now = Date.now();
|
|
24
|
-
const applicable = [];
|
|
25
|
-
for (const [key, cookie] of this.cookies) {
|
|
26
|
-
if (cookie.expiresAt !== undefined && cookie.expiresAt <= now) {
|
|
27
|
-
this.cookies.delete(key);
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (!cookieMatches(cookie, parsedUrl))
|
|
31
|
-
continue;
|
|
32
|
-
applicable.push(cookie);
|
|
33
|
-
}
|
|
34
|
-
applicable.sort((left, right) => right.path.length - left.path.length);
|
|
35
|
-
return applicable.length ? applicable.map(cookie => `${cookie.name}=${cookie.value}`).join('; ') : undefined;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/** HTTP-specific session boundary. One instance must be created per QAick execution. */
|
|
39
|
-
export class HttpExecutionSession {
|
|
40
|
-
transport;
|
|
41
|
-
cookies;
|
|
42
|
-
ambientCookies;
|
|
43
|
-
constructor(transport, cookies = new HttpCookieJar(), ambientCookies) {
|
|
44
|
-
this.transport = transport;
|
|
45
|
-
this.cookies = cookies;
|
|
46
|
-
this.ambientCookies = ambientCookies;
|
|
47
|
-
}
|
|
48
|
-
async request(url, init) {
|
|
49
|
-
let currentUrl = url;
|
|
50
|
-
let currentInit = { ...init };
|
|
51
|
-
for (let redirect = 0; redirect <= 10; redirect += 1) {
|
|
52
|
-
const headers = { ...(currentInit.headers ?? {}) };
|
|
53
|
-
const cookieHeader = headerName(headers, 'cookie');
|
|
54
|
-
const managed = this.cookies.header(currentUrl);
|
|
55
|
-
const ambient = this.ambientCookies ? await this.ambientCookies(currentUrl).catch(() => undefined) : undefined;
|
|
56
|
-
const merged = mergeCookieHeaders(cookieHeader ? headers[cookieHeader] : undefined, managed, ambient);
|
|
57
|
-
if (merged)
|
|
58
|
-
headers[cookieHeader ?? 'cookie'] = merged;
|
|
59
|
-
const response = await this.transport.request(currentUrl, { ...currentInit, headers });
|
|
60
|
-
const setCookie = response.headers && headerValue(response.headers, 'set-cookie');
|
|
61
|
-
this.cookies.store(currentUrl, setCookie);
|
|
62
|
-
if (!isRedirect(response.status) || !response.headers)
|
|
63
|
-
return response;
|
|
64
|
-
const location = headerValue(response.headers, 'location');
|
|
65
|
-
if (!location)
|
|
66
|
-
return response;
|
|
67
|
-
if (redirect === 10)
|
|
68
|
-
return response;
|
|
69
|
-
const nextUrl = new URL(location, currentUrl).toString();
|
|
70
|
-
const nextMethod = response.status === 303 || ((response.status === 301 || response.status === 302) && currentInit.method.toUpperCase() === 'POST') ? 'GET' : currentInit.method;
|
|
71
|
-
currentUrl = nextUrl;
|
|
72
|
-
currentInit = nextMethod === 'GET' ? { ...currentInit, method: nextMethod, body: undefined } : { ...currentInit, method: nextMethod };
|
|
73
|
-
}
|
|
74
|
-
throw new Error('HTTP redirect limit exceeded.');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
1
|
export class HttpActionError extends Error {
|
|
78
2
|
code;
|
|
79
3
|
details;
|
|
@@ -84,84 +8,6 @@ export class HttpActionError extends Error {
|
|
|
84
8
|
this.name = 'HttpActionError';
|
|
85
9
|
}
|
|
86
10
|
}
|
|
87
|
-
function headerName(headers, expected) {
|
|
88
|
-
return Object.keys(headers).find(name => name.toLowerCase() === expected);
|
|
89
|
-
}
|
|
90
|
-
function headerValue(headers, expected) {
|
|
91
|
-
const name = Object.keys(headers).find(key => key.toLowerCase() === expected);
|
|
92
|
-
return name ? headers[name] : undefined;
|
|
93
|
-
}
|
|
94
|
-
function isRedirect(status) { return status === 301 || status === 302 || status === 303 || status === 307 || status === 308; }
|
|
95
|
-
function mergeCookieHeaders(explicit, managed, ambient) {
|
|
96
|
-
const cookies = new Map();
|
|
97
|
-
for (const value of [ambient, managed, explicit]) {
|
|
98
|
-
for (const item of value?.split(';') ?? []) {
|
|
99
|
-
const separator = item.indexOf('=');
|
|
100
|
-
if (separator <= 0)
|
|
101
|
-
continue;
|
|
102
|
-
cookies.set(item.slice(0, separator).trim(), item.slice(separator + 1).trim());
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return cookies.size ? [...cookies.entries()].map(([name, value]) => `${name}=${value}`).join('; ') : undefined;
|
|
106
|
-
}
|
|
107
|
-
function splitSetCookieHeader(value) {
|
|
108
|
-
return value.split(/\r?\n|,(?=\s*[^;,=\s]+=[^;,]*)/).map(item => item.trim()).filter(Boolean);
|
|
109
|
-
}
|
|
110
|
-
function parseSetCookie(header, requestUrl) {
|
|
111
|
-
const parts = header.split(';').map(part => part.trim());
|
|
112
|
-
const pair = parts.shift();
|
|
113
|
-
if (!pair)
|
|
114
|
-
return undefined;
|
|
115
|
-
const separator = pair.indexOf('=');
|
|
116
|
-
if (separator <= 0)
|
|
117
|
-
return undefined;
|
|
118
|
-
const name = pair.slice(0, separator).trim();
|
|
119
|
-
const value = pair.slice(separator + 1).trim();
|
|
120
|
-
let domain = requestUrl.hostname.toLowerCase();
|
|
121
|
-
let hostOnly = true;
|
|
122
|
-
let path = defaultCookiePath(requestUrl.pathname);
|
|
123
|
-
let secure = false;
|
|
124
|
-
let expiresAt;
|
|
125
|
-
for (const attribute of parts) {
|
|
126
|
-
const [rawName, ...rawValue] = attribute.split('=');
|
|
127
|
-
const attributeName = rawName.trim().toLowerCase();
|
|
128
|
-
const attributeValue = rawValue.join('=').trim();
|
|
129
|
-
if (attributeName === 'domain' && attributeValue) {
|
|
130
|
-
domain = attributeValue.replace(/^\./, '').toLowerCase();
|
|
131
|
-
hostOnly = false;
|
|
132
|
-
}
|
|
133
|
-
else if (attributeName === 'path' && attributeValue.startsWith('/'))
|
|
134
|
-
path = attributeValue;
|
|
135
|
-
else if (attributeName === 'secure')
|
|
136
|
-
secure = true;
|
|
137
|
-
else if (attributeName === 'max-age') {
|
|
138
|
-
const seconds = Number(attributeValue);
|
|
139
|
-
if (Number.isFinite(seconds))
|
|
140
|
-
expiresAt = Date.now() + seconds * 1000;
|
|
141
|
-
}
|
|
142
|
-
else if (attributeName === 'expires') {
|
|
143
|
-
const timestamp = Date.parse(attributeValue);
|
|
144
|
-
if (Number.isFinite(timestamp))
|
|
145
|
-
expiresAt = timestamp;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return { name, value, domain, path, hostOnly, secure, ...(expiresAt !== undefined ? { expiresAt } : {}) };
|
|
149
|
-
}
|
|
150
|
-
function defaultCookiePath(pathname) {
|
|
151
|
-
if (!pathname || !pathname.startsWith('/'))
|
|
152
|
-
return '/';
|
|
153
|
-
const lastSlash = pathname.lastIndexOf('/');
|
|
154
|
-
return lastSlash <= 0 ? '/' : pathname.slice(0, lastSlash);
|
|
155
|
-
}
|
|
156
|
-
function cookieMatches(cookie, url) {
|
|
157
|
-
if (cookie.secure && url.protocol !== 'https:')
|
|
158
|
-
return false;
|
|
159
|
-
const host = url.hostname.toLowerCase();
|
|
160
|
-
if (cookie.hostOnly ? host !== cookie.domain : !(host === cookie.domain || host.endsWith(`.${cookie.domain}`)))
|
|
161
|
-
return false;
|
|
162
|
-
return url.pathname === cookie.path || url.pathname.startsWith(cookie.path.endsWith('/') ? cookie.path : `${cookie.path}/`);
|
|
163
|
-
}
|
|
164
|
-
function cookieKey(cookie) { return `${cookie.name};${cookie.domain};${cookie.path}`; }
|
|
165
11
|
export class FetchHttpTransport {
|
|
166
12
|
async request(url, init) {
|
|
167
13
|
const headers = { ...init.headers };
|
|
@@ -179,7 +25,7 @@ export class FetchHttpTransport {
|
|
|
179
25
|
const timeout = init.timeoutMs === undefined ? undefined : setTimeout(() => controller.abort(), init.timeoutMs);
|
|
180
26
|
let response;
|
|
181
27
|
try {
|
|
182
|
-
response = await fetch(url, { method: init.method, headers, body, signal: controller.signal
|
|
28
|
+
response = await fetch(url, { method: init.method, headers, body, signal: controller.signal });
|
|
183
29
|
}
|
|
184
30
|
catch (error) {
|
|
185
31
|
if (init.signal.aborted)
|
|
@@ -208,7 +54,6 @@ export class FetchHttpTransport {
|
|
|
208
54
|
}
|
|
209
55
|
export class HttpActionExecutor {
|
|
210
56
|
transport;
|
|
211
|
-
sessions = new Map();
|
|
212
57
|
constructor(transport) {
|
|
213
58
|
this.transport = transport;
|
|
214
59
|
}
|
|
@@ -223,7 +68,7 @@ export class HttpActionExecutor {
|
|
|
223
68
|
url: appendQuery(url, substituteTemplateValue(context.inputs.query, context.inputs)),
|
|
224
69
|
method: substituteTemplate(String(context.inputs.method ?? 'GET'), context.inputs),
|
|
225
70
|
...(context.inputs.body !== undefined ? { body: substituteTemplateValue(context.inputs.body, context.inputs) } : {}),
|
|
226
|
-
...(isRecord(context.inputs.headers) ? { headers:
|
|
71
|
+
...(isRecord(context.inputs.headers) || isRecord(context.inputs.cookies) ? { headers: renderHeaders(context.inputs.headers, context.inputs.cookies, context.inputs) } : {}),
|
|
227
72
|
};
|
|
228
73
|
}
|
|
229
74
|
catch (error) {
|
|
@@ -231,14 +76,14 @@ export class HttpActionExecutor {
|
|
|
231
76
|
throw new HttpActionError('HTTP_REQUEST_CONSTRUCTION_ERROR', `HTTP Action request could not be constructed. ${detail}`);
|
|
232
77
|
}
|
|
233
78
|
const timeoutMs = typeof context.step.adapterConfig?.timeoutMs === 'number' ? context.step.adapterConfig.timeoutMs : undefined;
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
const
|
|
79
|
+
const response = await this.transport.request(request.url, { ...request, signal: context.signal, ...(timeoutMs !== undefined ? { timeoutMs } : {}) });
|
|
80
|
+
const outputs = { status: response.status, body: response.body, ...(response.headers ? { headers: response.headers } : {}), cookies: responseCookies(response.headers) };
|
|
81
|
+
const responseHeadersName = context.step.adapterConfig?.responseHeadersName;
|
|
82
|
+
if (typeof responseHeadersName === 'string')
|
|
83
|
+
outputs[responseHeadersName] = response.headers ?? {};
|
|
237
84
|
if (response.status < 200 || response.status >= 300) {
|
|
238
|
-
|
|
239
|
-
throw new HttpActionError('HTTP_STATUS_ERROR', `HTTP Action returned status ${response.status}.${serializedBody ? ` Response: ${serializedBody.slice(0, 500)}` : ''}`, { status: response.status });
|
|
85
|
+
throw new HttpActionError('HTTP_STATUS_ERROR', `HTTP Action returned status ${response.status}. ${requestDiagnostics(request, response)}`, { status: response.status });
|
|
240
86
|
}
|
|
241
|
-
const outputs = { status: response.status, body: response.body, ...(response.headers ? { headers: response.headers } : {}) };
|
|
242
87
|
const responseFields = isRecord(context.step.adapterConfig?.responseFields) ? context.step.adapterConfig.responseFields : undefined;
|
|
243
88
|
if (responseFields)
|
|
244
89
|
for (const [name, path] of Object.entries(responseFields))
|
|
@@ -249,15 +94,65 @@ export class HttpActionExecutor {
|
|
|
249
94
|
const finishedAt = new Date().toISOString();
|
|
250
95
|
return { outputs, evidence: { request: { url: request.url, method: request.method, headers: request.headers }, response: { status: response.status, headers: response.headers, body: response.body } }, timing: { startedAt, finishedAt, durationMs: Date.now() - startedMs } };
|
|
251
96
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
97
|
+
}
|
|
98
|
+
function requestDiagnostics(request, response) {
|
|
99
|
+
const parsed = tryParseUrl(request.url);
|
|
100
|
+
const body = request.body === undefined ? undefined : typeof request.body === 'string' ? request.body : JSON.stringify(request.body) ?? '';
|
|
101
|
+
const responseBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body) ?? '';
|
|
102
|
+
return `Request diagnostics: method=${request.method} path=${parsed?.pathname ?? '(invalid)'} bodyBytes=${byteLength(body)} requestHeaderNames=${headerNames(request.headers).join(',') || '(none)'} responseHeaderNames=${headerNames(response.headers).join(',') || '(none)'} responseBodyBytes=${byteLength(responseBody)}.`;
|
|
103
|
+
}
|
|
104
|
+
function tryParseUrl(value) {
|
|
105
|
+
try {
|
|
106
|
+
return new URL(value);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
259
110
|
}
|
|
260
111
|
}
|
|
112
|
+
function headerNames(headers) {
|
|
113
|
+
return Object.keys(headers ?? {}).map(name => name.toLowerCase()).sort();
|
|
114
|
+
}
|
|
115
|
+
function byteLength(value) {
|
|
116
|
+
return value === undefined ? 0 : new TextEncoder().encode(value).byteLength;
|
|
117
|
+
}
|
|
118
|
+
function responseCookies(headers) {
|
|
119
|
+
const raw = headers && Object.entries(headers).find(([name]) => name.toLowerCase() === 'set-cookie')?.[1];
|
|
120
|
+
const values = Array.isArray(raw) ? raw : raw ? raw.split(/\r?\n|,(?=\s*[^;,=\s]+=[^;,]*)/).map(item => item.trim()).filter(Boolean) : [];
|
|
121
|
+
return Object.fromEntries(values.flatMap(header => {
|
|
122
|
+
const [pair, ...parts] = header.split(';').map(item => item.trim());
|
|
123
|
+
const separator = pair?.indexOf('=') ?? -1;
|
|
124
|
+
if (!pair || separator <= 0)
|
|
125
|
+
return [];
|
|
126
|
+
const name = pair.slice(0, separator).trim();
|
|
127
|
+
const value = pair.slice(separator + 1).trim();
|
|
128
|
+
const attributes = {};
|
|
129
|
+
for (const part of parts) {
|
|
130
|
+
const equals = part.indexOf('=');
|
|
131
|
+
if (equals > 0)
|
|
132
|
+
attributes[part.slice(0, equals).trim().toLowerCase()] = part.slice(equals + 1).trim();
|
|
133
|
+
else if (part)
|
|
134
|
+
attributes[part.toLowerCase()] = true;
|
|
135
|
+
}
|
|
136
|
+
return [[name, { value, attributes }]];
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
function renderHeaders(headersInput, cookiesInput, templateInputs) {
|
|
140
|
+
const headers = isRecord(headersInput)
|
|
141
|
+
? Object.fromEntries(Object.entries(substituteTemplateValue(headersInput, templateInputs)).map(([name, value]) => [name, String(value)]))
|
|
142
|
+
: {};
|
|
143
|
+
if (!isRecord(cookiesInput) || Object.keys(headers).some(name => name.toLowerCase() === 'cookie'))
|
|
144
|
+
return headers;
|
|
145
|
+
const cookies = Object.entries(cookiesInput).flatMap(([name, value]) => {
|
|
146
|
+
if (isRecord(value) && typeof value.value === 'string')
|
|
147
|
+
return [`${name}=${value.value}`];
|
|
148
|
+
if (typeof value === 'string')
|
|
149
|
+
return [`${name}=${value}`];
|
|
150
|
+
return [];
|
|
151
|
+
});
|
|
152
|
+
if (cookies.length)
|
|
153
|
+
headers.Cookie = cookies.join('; ');
|
|
154
|
+
return headers;
|
|
155
|
+
}
|
|
261
156
|
function appendQuery(url, query) {
|
|
262
157
|
if (query === undefined || query === null)
|
|
263
158
|
return url;
|
|
@@ -8,7 +8,7 @@ export class NodeHttpTransport {
|
|
|
8
8
|
if (init.signal.aborted)
|
|
9
9
|
return Promise.reject(new HttpActionError('HTTP_ABORTED', 'HTTP Action was cancelled before a response was received.'));
|
|
10
10
|
const parsed = new URL(url);
|
|
11
|
-
const headers = { ...init.headers };
|
|
11
|
+
const headers = { accept: '*/*', 'user-agent': 'QAICK-Desktop', ...init.headers };
|
|
12
12
|
let body;
|
|
13
13
|
if (init.body !== undefined && init.body !== null) {
|
|
14
14
|
body = typeof init.body === 'string' ? init.body : JSON.stringify(init.body);
|
|
@@ -76,7 +76,7 @@ export class NodeHttpTransport {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
function normalizeHeaders(headers) {
|
|
79
|
-
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, Array.isArray(value) ? value
|
|
79
|
+
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, Array.isArray(value) ? value : value ?? '']));
|
|
80
80
|
}
|
|
81
81
|
function headerValue(headers, name) {
|
|
82
82
|
const value = headers[name];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qaick/http-executor",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
4
|
"description": "Reusable QAick HTTP Action execution with browser and native Node transports.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qaick",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@qaick/executor": ">=0.1.0-alpha.1 <0.2.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@qaick/executor": "0.1.0-alpha.
|
|
45
|
+
"@qaick/executor": "0.1.0-alpha.2",
|
|
46
46
|
"@types/node": "^24.13.3",
|
|
47
47
|
"typescript": "~6.0.2",
|
|
48
48
|
"vitest": "^5.0.1"
|