@oxygen-agent/cli 1.226.15 → 1.242.6
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/README.md +1 -1
- package/dist/index.js +887 -12
- package/dist/local-custom-http-column.d.ts +2 -0
- package/dist/local-custom-http-column.js +347 -143
- package/node_modules/@oxygen/shared/dist/custom-http-safety.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/custom-http-safety.js +162 -0
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +62 -0
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +99 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-url.js +6 -1
- package/node_modules/@oxygen/shared/dist/select-options.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/select-options.js +11 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +104 -11
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +5 -0
- package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type CustomHttpResolveHostname } from "@oxygen/shared/custom-http-safety";
|
|
1
2
|
type LocalColumnRunOptions = {
|
|
2
3
|
rowId: string | null;
|
|
3
4
|
limit?: number;
|
|
4
5
|
concurrency?: number;
|
|
5
6
|
force: boolean;
|
|
7
|
+
resolveHostname?: CustomHttpResolveHostname;
|
|
6
8
|
};
|
|
7
9
|
export declare function runLocalCustomHttpColumn(table: string, columnKeyOrId: string, options: LocalColumnRunOptions): Promise<Record<string, unknown>>;
|
|
8
10
|
export {};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import { OxygenError } from "@oxygen/shared";
|
|
3
|
+
import { CustomHttpUrlSafetyError, assertCustomHttpPublicUrlSyntax, assertCustomHttpResolvedHostAllowed, } from "@oxygen/shared/custom-http-safety";
|
|
2
4
|
import { requestOxygen } from "./http-client.js";
|
|
5
|
+
const LOCAL_CUSTOM_HTTP_TIMEOUT_MS = 10_000;
|
|
6
|
+
const LOCAL_CUSTOM_HTTP_MAX_RESPONSE_BYTES = 1_000_000;
|
|
3
7
|
export async function runLocalCustomHttpColumn(table, columnKeyOrId, options) {
|
|
4
8
|
const described = await requestOxygen("/api/cli/tables/describe", {
|
|
5
9
|
method: "POST",
|
|
@@ -17,83 +21,20 @@ export async function runLocalCustomHttpColumn(table, columnKeyOrId, options) {
|
|
|
17
21
|
});
|
|
18
22
|
const rows = Array.isArray(queried.rows) ? queried.rows.filter(isRecord) : [];
|
|
19
23
|
const concurrency = normalizeLocalConcurrency(options.concurrency);
|
|
20
|
-
const results = await mapLocalConcurrent(rows, concurrency,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const execution = await executeLocalCustomHttpColumn(definition, row, secrets);
|
|
32
|
-
const update = await requestOxygen("/api/cli/tables/rows/update", {
|
|
33
|
-
method: "POST",
|
|
34
|
-
body: {
|
|
35
|
-
table,
|
|
36
|
-
row_id: rowId,
|
|
37
|
-
values: { [column.key]: execution.output },
|
|
38
|
-
metadata: {
|
|
39
|
-
command: "columns run --local",
|
|
40
|
-
column_key: column.key,
|
|
41
|
-
column_kind: "tool",
|
|
42
|
-
tool_mode: "custom_http",
|
|
43
|
-
request: execution.request,
|
|
44
|
-
response: execution.response,
|
|
45
|
-
http_status: execution.response.status,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
return {
|
|
50
|
-
status: "completed",
|
|
51
|
-
rowId,
|
|
52
|
-
toolId: "custom_http",
|
|
53
|
-
request: execution.request,
|
|
54
|
-
response: execution.response,
|
|
55
|
-
output: execution.output,
|
|
56
|
-
run: isRecord(update.run) ? update.run : null,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
const serialized = serializeLocalColumnError(error, "custom_http_run_failed");
|
|
61
|
-
const update = await requestOxygen("/api/cli/tables/rows/update", {
|
|
62
|
-
method: "POST",
|
|
63
|
-
body: {
|
|
64
|
-
table,
|
|
65
|
-
row_id: rowId,
|
|
66
|
-
values: { [column.key]: serialized },
|
|
67
|
-
metadata: {
|
|
68
|
-
command: "columns run --local",
|
|
69
|
-
column_key: column.key,
|
|
70
|
-
column_kind: "tool",
|
|
71
|
-
tool_mode: "custom_http",
|
|
72
|
-
error: serialized.error,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
return {
|
|
77
|
-
status: "failed",
|
|
78
|
-
rowId,
|
|
79
|
-
toolId: "custom_http",
|
|
80
|
-
output: serialized,
|
|
81
|
-
error: serialized.error,
|
|
82
|
-
run: isRecord(update.run) ? update.run : null,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
const completedCount = results.filter((result) => result.status === "completed").length;
|
|
87
|
-
const failedCount = results.filter((result) => result.status === "failed").length;
|
|
88
|
-
const skippedCount = results.filter((result) => result.status === "skipped").length;
|
|
89
|
-
const first = results[0];
|
|
24
|
+
const results = await mapLocalConcurrent(rows, concurrency, (row) => runLocalCustomHttpRow({
|
|
25
|
+
table,
|
|
26
|
+
columnKey: column.key,
|
|
27
|
+
definition,
|
|
28
|
+
row,
|
|
29
|
+
secrets,
|
|
30
|
+
force: options.force,
|
|
31
|
+
...(options.resolveHostname ? { resolveHostname: options.resolveHostname } : {}),
|
|
32
|
+
}));
|
|
33
|
+
const { completedCount, failedCount, skippedCount } = countLocalCustomHttpResults(results);
|
|
90
34
|
return {
|
|
91
|
-
status: failedCount
|
|
92
|
-
? "completed_with_errors"
|
|
93
|
-
: completedCount > 0
|
|
94
|
-
? "completed"
|
|
95
|
-
: "skipped",
|
|
35
|
+
status: localCustomHttpRunStatus({ completedCount, failedCount, skippedCount }),
|
|
96
36
|
table: described.table ?? queried.table ?? null,
|
|
37
|
+
web_url: localCustomHttpTableWebUrl(table, described.table, queried.table),
|
|
97
38
|
column,
|
|
98
39
|
toolId: "custom_http",
|
|
99
40
|
requestedRowCount: rows.length,
|
|
@@ -103,13 +44,123 @@ export async function runLocalCustomHttpColumn(table, columnKeyOrId, options) {
|
|
|
103
44
|
failedCount,
|
|
104
45
|
skippedCount,
|
|
105
46
|
results,
|
|
106
|
-
...(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
47
|
+
...localFirstRowFields(results[0]),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function runLocalCustomHttpRow(input) {
|
|
51
|
+
const rowId = readLocalRowId(input.row);
|
|
52
|
+
if (!input.force && isNonEmptyLocalValue(input.row[input.columnKey])) {
|
|
53
|
+
return {
|
|
54
|
+
status: "skipped",
|
|
55
|
+
rowId,
|
|
56
|
+
reason: "existing_value",
|
|
57
|
+
toolId: "custom_http",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const execution = await executeLocalCustomHttpColumn(input.definition, input.row, input.secrets, input.resolveHostname);
|
|
62
|
+
return await persistLocalCustomHttpSuccess({ ...input, rowId, execution });
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return await persistLocalCustomHttpFailure({ ...input, rowId, error });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function persistLocalCustomHttpSuccess(input) {
|
|
69
|
+
const update = await requestOxygen("/api/cli/tables/rows/update", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: {
|
|
72
|
+
table: input.table,
|
|
73
|
+
row_id: input.rowId,
|
|
74
|
+
values: { [input.columnKey]: input.execution.output },
|
|
75
|
+
metadata: {
|
|
76
|
+
command: "columns run --local",
|
|
77
|
+
column_key: input.columnKey,
|
|
78
|
+
column_kind: "tool",
|
|
79
|
+
tool_mode: "custom_http",
|
|
80
|
+
request: input.execution.request,
|
|
81
|
+
response: input.execution.response,
|
|
82
|
+
http_status: input.execution.response.status,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
status: "completed",
|
|
88
|
+
rowId: input.rowId,
|
|
89
|
+
toolId: "custom_http",
|
|
90
|
+
request: input.execution.request,
|
|
91
|
+
response: input.execution.response,
|
|
92
|
+
output: input.execution.output,
|
|
93
|
+
run: isRecord(update.run) ? update.run : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function persistLocalCustomHttpFailure(input) {
|
|
97
|
+
const serialized = serializeLocalColumnError(input.error, "custom_http_run_failed");
|
|
98
|
+
const update = await requestOxygen("/api/cli/tables/rows/update", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: {
|
|
101
|
+
table: input.table,
|
|
102
|
+
row_id: input.rowId,
|
|
103
|
+
values: { [input.columnKey]: serialized },
|
|
104
|
+
metadata: {
|
|
105
|
+
command: "columns run --local",
|
|
106
|
+
column_key: input.columnKey,
|
|
107
|
+
column_kind: "tool",
|
|
108
|
+
tool_mode: "custom_http",
|
|
109
|
+
error: serialized.error,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
status: "failed",
|
|
115
|
+
rowId: input.rowId,
|
|
116
|
+
toolId: "custom_http",
|
|
117
|
+
output: serialized,
|
|
118
|
+
error: serialized.error,
|
|
119
|
+
run: isRecord(update.run) ? update.run : null,
|
|
111
120
|
};
|
|
112
121
|
}
|
|
122
|
+
function countLocalCustomHttpResults(results) {
|
|
123
|
+
return {
|
|
124
|
+
completedCount: results.filter((result) => result.status === "completed").length,
|
|
125
|
+
failedCount: results.filter((result) => result.status === "failed").length,
|
|
126
|
+
skippedCount: results.filter((result) => result.status === "skipped").length,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function localCustomHttpRunStatus(counts) {
|
|
130
|
+
if (counts.failedCount > 0)
|
|
131
|
+
return "completed_with_errors";
|
|
132
|
+
if (counts.completedCount > 0)
|
|
133
|
+
return "completed";
|
|
134
|
+
return "skipped";
|
|
135
|
+
}
|
|
136
|
+
function localFirstRowFields(first) {
|
|
137
|
+
if (!first)
|
|
138
|
+
return {};
|
|
139
|
+
const fields = { rowId: first.rowId };
|
|
140
|
+
if ("request" in first)
|
|
141
|
+
fields.request = first.request;
|
|
142
|
+
if ("output" in first)
|
|
143
|
+
fields.output = first.output;
|
|
144
|
+
if ("error" in first)
|
|
145
|
+
fields.error = first.error;
|
|
146
|
+
if ("run" in first)
|
|
147
|
+
fields.run = first.run;
|
|
148
|
+
return fields;
|
|
149
|
+
}
|
|
150
|
+
function localCustomHttpTableWebUrl(table, described, queried) {
|
|
151
|
+
const identifier = readLocalTableIdentifier(described) ?? readLocalTableIdentifier(queried) ?? table;
|
|
152
|
+
return `https://oxygen-agent.com/tables/${encodeURIComponent(identifier)}`;
|
|
153
|
+
}
|
|
154
|
+
function readLocalTableIdentifier(table) {
|
|
155
|
+
if (!isRecord(table))
|
|
156
|
+
return null;
|
|
157
|
+
for (const key of ["id", "slug", "key"]) {
|
|
158
|
+
const value = table[key];
|
|
159
|
+
if (typeof value === "string" && value.trim())
|
|
160
|
+
return value.trim();
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
113
164
|
function findLocalColumn(columns, columnKeyOrId) {
|
|
114
165
|
const column = columns.find((entry) => entry.id === columnKeyOrId || entry.key === columnKeyOrId);
|
|
115
166
|
if (!column) {
|
|
@@ -283,47 +334,143 @@ async function mapLocalConcurrent(values, concurrency, action) {
|
|
|
283
334
|
}));
|
|
284
335
|
return results;
|
|
285
336
|
}
|
|
286
|
-
async function executeLocalCustomHttpColumn(definition, row, secrets) {
|
|
287
|
-
const
|
|
337
|
+
async function executeLocalCustomHttpColumn(definition, row, secrets, resolveHostname) {
|
|
338
|
+
const prepared = prepareLocalCustomHttpRequest(definition, row, secrets);
|
|
339
|
+
const response = await fetchLocalCustomHttpResponse(prepared, resolveHostname);
|
|
340
|
+
return await readLocalCustomHttpExecution(response, prepared, definition.outputPath ?? null);
|
|
341
|
+
}
|
|
342
|
+
function prepareLocalCustomHttpRequest(definition, row, secrets) {
|
|
343
|
+
const secretValues = expandLocalSecretValues(secrets);
|
|
288
344
|
const method = definition.request.method;
|
|
289
|
-
const
|
|
290
|
-
if (typeof resolvedUrl !== "string" || !resolvedUrl.trim()) {
|
|
291
|
-
throw new OxygenError("invalid_column_run", "Custom HTTP request url must resolve to a string.", {
|
|
292
|
-
details: { field: "request.url" },
|
|
293
|
-
exitCode: 1,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
const url = validateLocalHttpUrl(resolvedUrl, secretValues);
|
|
345
|
+
const url = resolveLocalHttpUrl(definition.request.url, row, secrets, secretValues);
|
|
297
346
|
const headers = resolveLocalHeaders(definition.request.headers ?? {}, row, secrets);
|
|
298
|
-
const bodyValue =
|
|
347
|
+
const bodyValue = resolveLocalHttpBodyValue(definition, row, secrets);
|
|
348
|
+
assertLocalHttpBodyAllowed(method, bodyValue);
|
|
349
|
+
const body = bodyValue === undefined ? undefined : serializeLocalHttpBody(bodyValue, headers);
|
|
350
|
+
return { method, url, headers, bodyValue, body, secretValues };
|
|
351
|
+
}
|
|
352
|
+
function expandLocalSecretValues(secrets) {
|
|
353
|
+
const values = new Set();
|
|
354
|
+
for (const secret of Object.values(secrets)) {
|
|
355
|
+
if (!secret)
|
|
356
|
+
continue;
|
|
357
|
+
values.add(secret);
|
|
358
|
+
try {
|
|
359
|
+
values.add(encodeURIComponent(secret));
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// Ignore values that cannot be URL-encoded.
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
values.add(encodeURI(secret));
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Ignore values that cannot be URL-encoded.
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return [...values];
|
|
372
|
+
}
|
|
373
|
+
function resolveLocalHttpUrl(template, row, secrets, secretValues) {
|
|
374
|
+
const resolvedUrl = resolveLocalTemplateString(template, row, secrets, "request.url");
|
|
375
|
+
if (typeof resolvedUrl === "string" && resolvedUrl.trim())
|
|
376
|
+
return validateLocalHttpUrl(resolvedUrl, secretValues);
|
|
377
|
+
throw new OxygenError("invalid_column_run", "Custom HTTP request url must resolve to a string.", {
|
|
378
|
+
details: { field: "request.url" },
|
|
379
|
+
exitCode: 1,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function resolveLocalHttpBodyValue(definition, row, secrets) {
|
|
383
|
+
return Object.hasOwn(definition.request, "body")
|
|
299
384
|
? resolveLocalTemplateValue(definition.request.body, row, secrets, "request.body")
|
|
300
385
|
: undefined;
|
|
386
|
+
}
|
|
387
|
+
function assertLocalHttpBodyAllowed(method, bodyValue) {
|
|
301
388
|
if (method === "GET" && bodyValue !== undefined) {
|
|
302
389
|
throw new OxygenError("invalid_column_run", "GET custom HTTP requests cannot include a body.", {
|
|
303
390
|
details: { method },
|
|
304
391
|
exitCode: 1,
|
|
305
392
|
});
|
|
306
393
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
394
|
+
}
|
|
395
|
+
async function fetchLocalCustomHttpResponse(prepared, resolveHostname) {
|
|
396
|
+
await assertLocalResolvedHttpUrlAllowed(prepared, resolveHostname);
|
|
397
|
+
return await fetch(prepared.url, {
|
|
398
|
+
method: prepared.method,
|
|
399
|
+
headers: prepared.headers,
|
|
400
|
+
...(prepared.body !== undefined ? { body: prepared.body } : {}),
|
|
401
|
+
redirect: "manual",
|
|
402
|
+
signal: AbortSignal.timeout(LOCAL_CUSTOM_HTTP_TIMEOUT_MS),
|
|
312
403
|
}).catch((error) => {
|
|
313
404
|
throw new OxygenError("custom_http_network_error", "Custom HTTP request failed before receiving a response.", {
|
|
314
405
|
details: {
|
|
315
|
-
reason: redactLocalSecrets(error instanceof Error ? error.message : String(error), secretValues),
|
|
406
|
+
reason: redactLocalSecrets(error instanceof Error ? error.message : String(error), prepared.secretValues),
|
|
316
407
|
},
|
|
317
408
|
exitCode: 1,
|
|
318
409
|
});
|
|
319
410
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
411
|
+
}
|
|
412
|
+
async function assertLocalResolvedHttpUrlAllowed(prepared, resolveHostname) {
|
|
413
|
+
try {
|
|
414
|
+
await assertCustomHttpResolvedHostAllowed({
|
|
415
|
+
url: prepared.url,
|
|
416
|
+
...(resolveHostname ? { resolveHostname } : {}),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (!(error instanceof CustomHttpUrlSafetyError))
|
|
421
|
+
throw error;
|
|
422
|
+
if (error.reason === "dns_lookup_failed") {
|
|
423
|
+
throw new OxygenError("custom_http_network_error", "Custom HTTP request failed before receiving a response.", {
|
|
424
|
+
details: {
|
|
425
|
+
reason: redactLocalSecrets(error.message, prepared.secretValues),
|
|
426
|
+
},
|
|
427
|
+
exitCode: 1,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
throw new OxygenError("invalid_column_run", "Custom HTTP request url is invalid.", {
|
|
431
|
+
details: {
|
|
432
|
+
reason: redactLocalSecrets(error.message, prepared.secretValues),
|
|
433
|
+
},
|
|
434
|
+
exitCode: 1,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function readLocalCustomHttpExecution(response, prepared, outputPath) {
|
|
439
|
+
assertLocalNoRedirect(response, prepared.secretValues);
|
|
440
|
+
const responseBody = await readLocalHttpResponseBody(response, LOCAL_CUSTOM_HTTP_MAX_RESPONSE_BYTES);
|
|
441
|
+
const redactedResponseBody = redactLocalSecrets(responseBody, prepared.secretValues);
|
|
442
|
+
const responseSummary = summarizeLocalHttpResponse(response, prepared.secretValues);
|
|
443
|
+
assertLocalOkResponse(response, redactedResponseBody);
|
|
444
|
+
return {
|
|
445
|
+
output: applyLocalOutputPath(redactedResponseBody, outputPath),
|
|
446
|
+
request: redactLocalSecrets({
|
|
447
|
+
method: prepared.method,
|
|
448
|
+
url: prepared.url,
|
|
449
|
+
...(Object.keys(prepared.headers).length > 0 ? { headers: prepared.headers } : {}),
|
|
450
|
+
...(prepared.bodyValue !== undefined ? { body: prepared.bodyValue } : {}),
|
|
451
|
+
}, prepared.secretValues),
|
|
452
|
+
response: responseSummary,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function assertLocalNoRedirect(response, secretValues) {
|
|
456
|
+
if (response.status >= 300 && response.status < 400) {
|
|
457
|
+
throw new OxygenError("custom_http_redirect_blocked", "Custom HTTP redirects are blocked for safety.", {
|
|
458
|
+
details: {
|
|
459
|
+
status: response.status,
|
|
460
|
+
location: redactLocalSecrets(response.headers.get("location") ?? null, secretValues),
|
|
461
|
+
},
|
|
462
|
+
exitCode: 1,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function summarizeLocalHttpResponse(response, secretValues) {
|
|
467
|
+
return {
|
|
323
468
|
status: response.status,
|
|
324
469
|
statusText: response.statusText,
|
|
325
470
|
headers: redactLocalSecrets(localHeadersToObject(response.headers), secretValues),
|
|
326
471
|
};
|
|
472
|
+
}
|
|
473
|
+
function assertLocalOkResponse(response, redactedResponseBody) {
|
|
327
474
|
if (!response.ok) {
|
|
328
475
|
throw new OxygenError("custom_http_request_failed", "Custom HTTP request returned a non-2xx response.", {
|
|
329
476
|
details: {
|
|
@@ -334,22 +481,11 @@ async function executeLocalCustomHttpColumn(definition, row, secrets) {
|
|
|
334
481
|
exitCode: 1,
|
|
335
482
|
});
|
|
336
483
|
}
|
|
337
|
-
return {
|
|
338
|
-
output: applyLocalOutputPath(redactedResponseBody, definition.outputPath ?? null),
|
|
339
|
-
request: redactLocalSecrets({
|
|
340
|
-
method,
|
|
341
|
-
url,
|
|
342
|
-
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
343
|
-
...(bodyValue !== undefined ? { body: bodyValue } : {}),
|
|
344
|
-
}, secretValues),
|
|
345
|
-
response: responseSummary,
|
|
346
|
-
};
|
|
347
484
|
}
|
|
348
485
|
function validateLocalHttpUrl(rawUrl, secretValues) {
|
|
349
486
|
try {
|
|
350
487
|
const parsed = new URL(rawUrl);
|
|
351
|
-
|
|
352
|
-
throw new Error("Unsupported protocol.");
|
|
488
|
+
validateLocalPublicHttpsUrl(parsed);
|
|
353
489
|
return parsed.toString();
|
|
354
490
|
}
|
|
355
491
|
catch (error) {
|
|
@@ -362,6 +498,16 @@ function validateLocalHttpUrl(rawUrl, secretValues) {
|
|
|
362
498
|
});
|
|
363
499
|
}
|
|
364
500
|
}
|
|
501
|
+
function validateLocalPublicHttpsUrl(parsed) {
|
|
502
|
+
try {
|
|
503
|
+
assertCustomHttpPublicUrlSyntax(parsed);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (error instanceof CustomHttpUrlSafetyError)
|
|
507
|
+
throw new Error(error.message);
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
365
511
|
function resolveLocalHeaders(headers, row, secrets) {
|
|
366
512
|
const resolved = {};
|
|
367
513
|
for (const [key, value] of Object.entries(headers)) {
|
|
@@ -382,8 +528,17 @@ function hasLocalHeader(headers, name) {
|
|
|
382
528
|
const normalized = name.toLowerCase();
|
|
383
529
|
return Object.keys(headers).some((key) => key.toLowerCase() === normalized);
|
|
384
530
|
}
|
|
385
|
-
async function readLocalHttpResponseBody(response) {
|
|
386
|
-
const
|
|
531
|
+
async function readLocalHttpResponseBody(response, maxBytes) {
|
|
532
|
+
const contentLength = response.headers.get("content-length");
|
|
533
|
+
const declaredLength = contentLength === null ? null : Number(contentLength);
|
|
534
|
+
if (declaredLength !== null && Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
535
|
+
throw new OxygenError("custom_http_response_too_large", "Custom HTTP response exceeds the configured size cap.", {
|
|
536
|
+
details: { max_response_bytes: maxBytes, content_length: declaredLength },
|
|
537
|
+
exitCode: 1,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const buffer = await readLocalHttpResponseBuffer(response, maxBytes);
|
|
541
|
+
const text = buffer.toString("utf8");
|
|
387
542
|
if (!text)
|
|
388
543
|
return null;
|
|
389
544
|
const contentType = response.headers.get("content-type") ?? "";
|
|
@@ -403,6 +558,41 @@ async function readLocalHttpResponseBody(response) {
|
|
|
403
558
|
return text;
|
|
404
559
|
}
|
|
405
560
|
}
|
|
561
|
+
async function readLocalHttpResponseBuffer(response, maxBytes) {
|
|
562
|
+
if (!response.body) {
|
|
563
|
+
const buffer = await response.arrayBuffer();
|
|
564
|
+
if (buffer.byteLength > maxBytes) {
|
|
565
|
+
throw new OxygenError("custom_http_response_too_large", "Custom HTTP response exceeds the configured size cap.", {
|
|
566
|
+
details: { max_response_bytes: maxBytes, actual_bytes: buffer.byteLength },
|
|
567
|
+
exitCode: 1,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return Buffer.from(buffer);
|
|
571
|
+
}
|
|
572
|
+
const reader = response.body.getReader();
|
|
573
|
+
const chunks = [];
|
|
574
|
+
let actualBytes = 0;
|
|
575
|
+
try {
|
|
576
|
+
while (true) {
|
|
577
|
+
const { done, value } = await reader.read();
|
|
578
|
+
if (done)
|
|
579
|
+
break;
|
|
580
|
+
actualBytes += value.byteLength;
|
|
581
|
+
if (actualBytes > maxBytes) {
|
|
582
|
+
await reader.cancel().catch(() => undefined);
|
|
583
|
+
throw new OxygenError("custom_http_response_too_large", "Custom HTTP response exceeds the configured size cap.", {
|
|
584
|
+
details: { max_response_bytes: maxBytes, actual_bytes: actualBytes },
|
|
585
|
+
exitCode: 1,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
chunks.push(Buffer.from(value));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
reader.releaseLock();
|
|
593
|
+
}
|
|
594
|
+
return Buffer.concat(chunks, actualBytes);
|
|
595
|
+
}
|
|
406
596
|
function localHeadersToObject(headers) {
|
|
407
597
|
const result = {};
|
|
408
598
|
headers.forEach((value, key) => {
|
|
@@ -434,10 +624,17 @@ function resolveLocalTemplateString(value, row, secrets, inputName) {
|
|
|
434
624
|
});
|
|
435
625
|
}
|
|
436
626
|
function resolveLocalTemplatePath(rawPath, row, secrets, inputName) {
|
|
437
|
-
const segments = rawPath
|
|
627
|
+
const segments = splitLocalTemplatePath(rawPath);
|
|
628
|
+
const root = resolveLocalTemplateRoot(segments, row, secrets, inputName, rawPath);
|
|
629
|
+
return resolveLocalTemplateSegments(root.current, root.segments);
|
|
630
|
+
}
|
|
631
|
+
function splitLocalTemplatePath(rawPath) {
|
|
632
|
+
return rawPath
|
|
438
633
|
.split(".")
|
|
439
634
|
.map((segment) => segment.trim())
|
|
440
635
|
.filter(Boolean);
|
|
636
|
+
}
|
|
637
|
+
function resolveLocalTemplateRoot(segments, row, secrets, inputName, rawPath) {
|
|
441
638
|
const root = segments.shift();
|
|
442
639
|
if (!root) {
|
|
443
640
|
throw new OxygenError("invalid_column_run", "Template reference is empty.", {
|
|
@@ -445,40 +642,47 @@ function resolveLocalTemplatePath(rawPath, row, secrets, inputName) {
|
|
|
445
642
|
exitCode: 1,
|
|
446
643
|
});
|
|
447
644
|
}
|
|
448
|
-
let current;
|
|
449
645
|
if (root === "secrets") {
|
|
450
|
-
|
|
451
|
-
if (!secretName || !Object.hasOwn(secrets, secretName)) {
|
|
452
|
-
throw new OxygenError("invalid_column_run", "Template references an unknown custom HTTP secret.", {
|
|
453
|
-
details: { input: inputName, secret: secretName ?? null, template: rawPath },
|
|
454
|
-
exitCode: 1,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
current = secrets[secretName];
|
|
646
|
+
return resolveLocalTemplateSecretRoot(segments, secrets, inputName, rawPath);
|
|
458
647
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
648
|
+
return resolveLocalTemplateRowRoot(root, segments, row, inputName, rawPath);
|
|
649
|
+
}
|
|
650
|
+
function resolveLocalTemplateSecretRoot(segments, secrets, inputName, rawPath) {
|
|
651
|
+
const secretName = segments.shift();
|
|
652
|
+
if (!secretName || !Object.hasOwn(secrets, secretName)) {
|
|
653
|
+
throw new OxygenError("invalid_column_run", "Template references an unknown custom HTTP secret.", {
|
|
654
|
+
details: { input: inputName, secret: secretName ?? null, template: rawPath },
|
|
655
|
+
exitCode: 1,
|
|
656
|
+
});
|
|
467
657
|
}
|
|
658
|
+
return { current: secrets[secretName], segments };
|
|
659
|
+
}
|
|
660
|
+
function resolveLocalTemplateRowRoot(root, segments, row, inputName, rawPath) {
|
|
661
|
+
if (!Object.hasOwn(row, root)) {
|
|
662
|
+
throw new OxygenError("invalid_column_run", "Template references an unknown row column.", {
|
|
663
|
+
details: { input: inputName, column_key: root, template: rawPath },
|
|
664
|
+
exitCode: 1,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return { current: row[root], segments };
|
|
668
|
+
}
|
|
669
|
+
function resolveLocalTemplateSegments(current, segments) {
|
|
670
|
+
let value = current;
|
|
468
671
|
for (const segment of segments) {
|
|
469
|
-
|
|
672
|
+
value = resolveLocalTemplateSegment(value, segment);
|
|
673
|
+
if (value === null)
|
|
470
674
|
return null;
|
|
471
|
-
if (Array.isArray(current) && /^\d+$/.test(segment)) {
|
|
472
|
-
current = current[Number(segment)] ?? null;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
if (isRecord(current) && Object.hasOwn(current, segment)) {
|
|
476
|
-
current = current[segment];
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
return null;
|
|
480
675
|
}
|
|
481
|
-
return
|
|
676
|
+
return value ?? null;
|
|
677
|
+
}
|
|
678
|
+
function resolveLocalTemplateSegment(current, segment) {
|
|
679
|
+
if (current === null || current === undefined)
|
|
680
|
+
return null;
|
|
681
|
+
if (Array.isArray(current) && /^\d+$/.test(segment))
|
|
682
|
+
return current[Number(segment)] ?? null;
|
|
683
|
+
if (isRecord(current) && Object.hasOwn(current, segment))
|
|
684
|
+
return current[segment];
|
|
685
|
+
return null;
|
|
482
686
|
}
|
|
483
687
|
function applyLocalOutputPath(value, outputPath) {
|
|
484
688
|
const path = typeof outputPath === "string" ? outputPath.trim() : "";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type CustomHttpResolvedAddress = {
|
|
2
|
+
address: string;
|
|
3
|
+
family?: number;
|
|
4
|
+
};
|
|
5
|
+
export type CustomHttpResolveHostname = (hostname: string) => Promise<CustomHttpResolvedAddress[]>;
|
|
6
|
+
export type CustomHttpUrlSafetyErrorReason = "blocked_host" | "blocked_resolved_address" | "credentials" | "dns_lookup_failed" | "protocol";
|
|
7
|
+
export declare class CustomHttpUrlSafetyError extends Error {
|
|
8
|
+
readonly reason: CustomHttpUrlSafetyErrorReason;
|
|
9
|
+
readonly details: Record<string, unknown>;
|
|
10
|
+
constructor(reason: CustomHttpUrlSafetyErrorReason, message: string, details?: Record<string, unknown>);
|
|
11
|
+
}
|
|
12
|
+
export declare function assertCustomHttpPublicUrlSyntax(url: string | URL): URL;
|
|
13
|
+
export declare function assertCustomHttpResolvedHostAllowed(input: {
|
|
14
|
+
url: string | URL;
|
|
15
|
+
resolveHostname?: CustomHttpResolveHostname;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
export declare function normalizeCustomHttpUrlHost(hostname: string): string;
|
|
18
|
+
export declare function isBlockedCustomHttpHost(host: string): boolean;
|