@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.
@@ -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, async (row) => {
21
- const rowId = readLocalRowId(row);
22
- if (!options.force && isNonEmptyLocalValue(row[column.key])) {
23
- return {
24
- status: "skipped",
25
- rowId,
26
- reason: "existing_value",
27
- toolId: "custom_http",
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 > 0
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
- ...(first ? { rowId: first.rowId } : {}),
107
- ...(first && "request" in first ? { request: first.request } : {}),
108
- ...(first && "output" in first ? { output: first.output } : {}),
109
- ...(first && "error" in first ? { error: first.error } : {}),
110
- ...(first && "run" in first ? { run: first.run } : {}),
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 secretValues = Object.values(secrets);
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 resolvedUrl = resolveLocalTemplateString(definition.request.url, row, secrets, "request.url");
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 = Object.hasOwn(definition.request, "body")
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
- const body = bodyValue === undefined ? undefined : serializeLocalHttpBody(bodyValue, headers);
308
- const response = await fetch(url, {
309
- method,
310
- headers,
311
- ...(body !== undefined ? { body } : {}),
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
- const responseBody = await readLocalHttpResponseBody(response);
321
- const redactedResponseBody = redactLocalSecrets(responseBody, secretValues);
322
- const responseSummary = {
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
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
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 text = await response.text();
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
- const secretName = segments.shift();
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
- else {
460
- if (!Object.hasOwn(row, root)) {
461
- throw new OxygenError("invalid_column_run", "Template references an unknown row column.", {
462
- details: { input: inputName, column_key: root, template: rawPath },
463
- exitCode: 1,
464
- });
465
- }
466
- current = row[root];
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
- if (current === null || current === undefined)
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 current ?? null;
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;