deepline 0.1.133 → 0.1.135
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/bundling-sources/apps/play-runner-workers/src/entry.ts +234 -24
- package/dist/bundling-sources/sdk/src/play.ts +4 -1
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/builtin-pacing.ts +48 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +88 -58
- package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +13 -0
- package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +70 -18
- package/dist/cli/index.js +26 -18
- package/dist/cli/index.mjs +26 -18
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
|
@@ -49,6 +49,11 @@ import {
|
|
|
49
49
|
type GovernanceSnapshot,
|
|
50
50
|
type PlayExecutionGovernor,
|
|
51
51
|
} from '../../../shared_libs/play-runtime/governor/governor';
|
|
52
|
+
import {
|
|
53
|
+
CTX_FETCH_EGRESS_PROVIDER,
|
|
54
|
+
CTX_FETCH_EGRESS_TOOL_ID,
|
|
55
|
+
resolveBuiltinPacing,
|
|
56
|
+
} from '../../../shared_libs/play-runtime/builtin-pacing';
|
|
52
57
|
import {
|
|
53
58
|
CoordinatorRateStateBackend,
|
|
54
59
|
type CoordinatorRatePort,
|
|
@@ -151,6 +156,7 @@ import { normalizePlayRunFailure } from '../../../shared_libs/play-runtime/run-f
|
|
|
151
156
|
import { createSecretRedactionContext } from '../../../shared_libs/play-runtime/secret-redaction';
|
|
152
157
|
import {
|
|
153
158
|
assertNoSecretTaint,
|
|
159
|
+
assertSecretAuthUsesTls,
|
|
154
160
|
createBearerSecretAuth,
|
|
155
161
|
createHeaderSecretAuth,
|
|
156
162
|
createSecretHandle,
|
|
@@ -165,6 +171,7 @@ import { safePublicFetch } from '../../../shared_libs/security/safe-fetch';
|
|
|
165
171
|
import {
|
|
166
172
|
assertPublicHttpUrl,
|
|
167
173
|
isIpAddressLiteral,
|
|
174
|
+
normalizeUrlHostname,
|
|
168
175
|
UnsafeOutboundUrlError,
|
|
169
176
|
} from '../../../shared_libs/security/outbound-url-policy';
|
|
170
177
|
import type {
|
|
@@ -475,6 +482,7 @@ async function probeHarnessOnce(
|
|
|
475
482
|
*/
|
|
476
483
|
const RUNTIME_API_TIMEOUT_MS = 30_000;
|
|
477
484
|
const RUNTIME_API_PLAY_RUN_TIMEOUT_MS = 75_000;
|
|
485
|
+
const RUNTIME_API_EGRESS_FETCH_TIMEOUT_MS = 180_000;
|
|
478
486
|
const RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS = 180_000;
|
|
479
487
|
const RUNTIME_API_RETRY_DELAYS_MS = [
|
|
480
488
|
250, 750, 1500, 3000, 5000, 10000,
|
|
@@ -488,9 +496,11 @@ async function fetchRuntimeApi(
|
|
|
488
496
|
const timeoutMs =
|
|
489
497
|
path === '/api/v2/plays/run'
|
|
490
498
|
? RUNTIME_API_PLAY_RUN_TIMEOUT_MS
|
|
491
|
-
:
|
|
492
|
-
?
|
|
493
|
-
:
|
|
499
|
+
: path === '/api/v2/plays/internal/egress-fetch'
|
|
500
|
+
? RUNTIME_API_EGRESS_FETCH_TIMEOUT_MS
|
|
501
|
+
: /^\/api\/v2\/integrations\/[^/]+\/execute$/.test(path)
|
|
502
|
+
? RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS
|
|
503
|
+
: RUNTIME_API_TIMEOUT_MS;
|
|
494
504
|
const controller = new AbortController();
|
|
495
505
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
496
506
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
@@ -2313,6 +2323,20 @@ type WorkerFetchResponse = {
|
|
|
2313
2323
|
json: unknown | null;
|
|
2314
2324
|
};
|
|
2315
2325
|
|
|
2326
|
+
type WorkerFetchBodyDescriptor =
|
|
2327
|
+
| { kind: 'none' }
|
|
2328
|
+
| { kind: 'string'; value: string }
|
|
2329
|
+
| { kind: 'urlSearchParams'; value: string };
|
|
2330
|
+
|
|
2331
|
+
type RuntimeEgressFetchPayload = {
|
|
2332
|
+
url: string;
|
|
2333
|
+
method: string;
|
|
2334
|
+
headers: Record<string, string>;
|
|
2335
|
+
body: WorkerFetchBodyDescriptor;
|
|
2336
|
+
redirect?: RequestInit['redirect'];
|
|
2337
|
+
sensitiveHeaders: string[];
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2316
2340
|
function normalizeFetchHeaders(
|
|
2317
2341
|
headers: RequestInit['headers'],
|
|
2318
2342
|
): Record<string, string> {
|
|
@@ -2344,6 +2368,19 @@ function fetchBodyIdentity(body: RequestInit['body']): string | null {
|
|
|
2344
2368
|
);
|
|
2345
2369
|
}
|
|
2346
2370
|
|
|
2371
|
+
function fetchBodyDescriptor(
|
|
2372
|
+
body: RequestInit['body'],
|
|
2373
|
+
): WorkerFetchBodyDescriptor {
|
|
2374
|
+
if (body === undefined || body === null) return { kind: 'none' };
|
|
2375
|
+
if (typeof body === 'string') return { kind: 'string', value: body };
|
|
2376
|
+
if (body instanceof URLSearchParams) {
|
|
2377
|
+
return { kind: 'urlSearchParams', value: body.toString() };
|
|
2378
|
+
}
|
|
2379
|
+
throw new Error(
|
|
2380
|
+
'ctx.fetch(...) in the Workers backend only supports string or URLSearchParams request bodies for egress.',
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2347
2384
|
function parseFetchJsonOrNull(bodyText: string): unknown | null {
|
|
2348
2385
|
if (!bodyText.trim()) return null;
|
|
2349
2386
|
try {
|
|
@@ -2371,7 +2408,7 @@ async function safeWorkerPublicFetch(
|
|
|
2371
2408
|
!allowedOrigins.has(url.origin)
|
|
2372
2409
|
) {
|
|
2373
2410
|
throw new UnsafeOutboundUrlError(
|
|
2374
|
-
'workers_edge
|
|
2411
|
+
'workers_edge direct fetch is reserved for public IP literals and Deepline runtime origins. Public hostnames must go through the Deepline egress endpoint with safeOutboundFetch.',
|
|
2375
2412
|
);
|
|
2376
2413
|
}
|
|
2377
2414
|
return fetch(url, nextInit);
|
|
@@ -2379,6 +2416,121 @@ async function safeWorkerPublicFetch(
|
|
|
2379
2416
|
});
|
|
2380
2417
|
}
|
|
2381
2418
|
|
|
2419
|
+
function isDeeplineControlledWorkerFetchHost(hostname: string): boolean {
|
|
2420
|
+
const normalized = normalizeUrlHostname(hostname);
|
|
2421
|
+
return (
|
|
2422
|
+
normalized === 'deepline.com' ||
|
|
2423
|
+
normalized.endsWith('.deepline.com') ||
|
|
2424
|
+
normalized === 'deeplinedeveloper.com' ||
|
|
2425
|
+
normalized.endsWith('.deeplinedeveloper.com')
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
function shouldUseRuntimeEgressFetch(
|
|
2430
|
+
input: string | URL,
|
|
2431
|
+
allowedOrigins: Iterable<string>,
|
|
2432
|
+
): boolean {
|
|
2433
|
+
const url = assertPublicHttpUrl(input);
|
|
2434
|
+
const allowedOriginSet = new Set(allowedOrigins);
|
|
2435
|
+
if (allowedOriginSet.has(url.origin)) return false;
|
|
2436
|
+
if (isIpAddressLiteral(url.hostname)) return false;
|
|
2437
|
+
if (isDeeplineControlledWorkerFetchHost(url.hostname)) {
|
|
2438
|
+
throw new UnsafeOutboundUrlError(
|
|
2439
|
+
'ctx.fetch cannot call a non-runtime Deepline-owned origin from workers_edge. Use the run runtime origin or a Deepline integration tool.',
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
return true;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
function shouldPaceWorkerEgressFetch(
|
|
2446
|
+
input: string | URL,
|
|
2447
|
+
allowedOrigins: Iterable<string>,
|
|
2448
|
+
): boolean {
|
|
2449
|
+
const url = assertPublicHttpUrl(input);
|
|
2450
|
+
return !new Set(allowedOrigins).has(url.origin);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function isWorkerFetchResponse(value: unknown): value is WorkerFetchResponse {
|
|
2454
|
+
if (!isRecord(value)) return false;
|
|
2455
|
+
return (
|
|
2456
|
+
typeof value.ok === 'boolean' &&
|
|
2457
|
+
typeof value.status === 'number' &&
|
|
2458
|
+
typeof value.statusText === 'string' &&
|
|
2459
|
+
typeof value.url === 'string' &&
|
|
2460
|
+
isRecord(value.headers) &&
|
|
2461
|
+
Object.values(value.headers).every((entry) => typeof entry === 'string') &&
|
|
2462
|
+
typeof value.bodyText === 'string' &&
|
|
2463
|
+
'json' in value
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
async function postRuntimeEgressFetch(
|
|
2468
|
+
req: RunRequest,
|
|
2469
|
+
payload: RuntimeEgressFetchPayload,
|
|
2470
|
+
onProviderBackpressure?: (retryAfterMs: number) => void,
|
|
2471
|
+
onRetryAttempt?: () => void,
|
|
2472
|
+
): Promise<WorkerFetchResponse> {
|
|
2473
|
+
let lastError: Error | null = null;
|
|
2474
|
+
for (
|
|
2475
|
+
let attempt = 1;
|
|
2476
|
+
attempt <= WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS;
|
|
2477
|
+
attempt += 1
|
|
2478
|
+
) {
|
|
2479
|
+
const response = await fetchRuntimeApi(
|
|
2480
|
+
req.baseUrl,
|
|
2481
|
+
'/api/v2/plays/internal/egress-fetch',
|
|
2482
|
+
{
|
|
2483
|
+
method: 'POST',
|
|
2484
|
+
headers: {
|
|
2485
|
+
authorization: `Bearer ${req.executorToken}`,
|
|
2486
|
+
'content-type': 'application/json',
|
|
2487
|
+
},
|
|
2488
|
+
body: JSON.stringify(payload),
|
|
2489
|
+
},
|
|
2490
|
+
);
|
|
2491
|
+
const bodyText = await response.text();
|
|
2492
|
+
let parsed: unknown = null;
|
|
2493
|
+
try {
|
|
2494
|
+
parsed = bodyText.trim() ? JSON.parse(bodyText) : null;
|
|
2495
|
+
} catch {
|
|
2496
|
+
parsed = null;
|
|
2497
|
+
}
|
|
2498
|
+
if (response.ok) {
|
|
2499
|
+
if (!isWorkerFetchResponse(parsed)) {
|
|
2500
|
+
throw new Error(
|
|
2501
|
+
'ctx.fetch egress returned an invalid response payload.',
|
|
2502
|
+
);
|
|
2503
|
+
}
|
|
2504
|
+
return parsed;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const message =
|
|
2508
|
+
isRecord(parsed) && typeof parsed.error === 'string'
|
|
2509
|
+
? parsed.error
|
|
2510
|
+
: bodyText || `HTTP ${response.status}`;
|
|
2511
|
+
lastError = new Error(`ctx.fetch egress failed: ${message}`);
|
|
2512
|
+
if (response.status !== 429) {
|
|
2513
|
+
throw lastError;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
const retryAfterSeconds = Number(response.headers.get('retry-after'));
|
|
2517
|
+
const retryAfterMs =
|
|
2518
|
+
Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
|
|
2519
|
+
? Math.ceil(retryAfterSeconds * 1000)
|
|
2520
|
+
: 1_000;
|
|
2521
|
+
onProviderBackpressure?.(retryAfterMs);
|
|
2522
|
+
if (attempt >= WORKER_TOOL_RATE_LIMIT_MAX_ATTEMPTS) {
|
|
2523
|
+
throw lastError;
|
|
2524
|
+
}
|
|
2525
|
+
onRetryAttempt?.();
|
|
2526
|
+
await sleepWorkerMs(
|
|
2527
|
+
Math.min(5_000, Math.max(retryAfterMs, attempt * 1000)),
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
throw lastError ?? new Error('ctx.fetch egress failed before execution.');
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2382
2534
|
function normalizeAllowedWorkerFetchOrigin(rawUrl: string): string | null {
|
|
2383
2535
|
try {
|
|
2384
2536
|
return assertPublicHttpUrl(rawUrl).origin;
|
|
@@ -3172,6 +3324,8 @@ function createWorkerPacingResolver(req: RunRequest): WorkerPacingResolver {
|
|
|
3172
3324
|
return (toolId: string) => {
|
|
3173
3325
|
const normalized = String(toolId || '').trim();
|
|
3174
3326
|
if (!normalized) return Promise.resolve(null);
|
|
3327
|
+
const builtin = resolveBuiltinPacing(normalized);
|
|
3328
|
+
if (builtin) return Promise.resolve(builtin);
|
|
3175
3329
|
const cached = cache.get(normalized);
|
|
3176
3330
|
if (cached) return cached;
|
|
3177
3331
|
const promise = (async () => {
|
|
@@ -5378,6 +5532,11 @@ function createMinimalWorkerCtx(
|
|
|
5378
5532
|
if (init.auth !== undefined && !isSecretAuth(init.auth)) {
|
|
5379
5533
|
throw new Error('ctx.fetch auth must come from ctx.secrets.');
|
|
5380
5534
|
}
|
|
5535
|
+
// Keep the boundary unmistakable: customer code may intentionally attach a
|
|
5536
|
+
// customer-owned secret to an outbound auth header, but Deepline must never
|
|
5537
|
+
// send that secret over plaintext HTTP. Non-secret arbitrary HTTP egress can
|
|
5538
|
+
// still use the generic_http lane; secret-bearing ctx.fetch requires TLS.
|
|
5539
|
+
assertSecretAuthUsesTls(init.auth, input, 'ctx.fetch');
|
|
5381
5540
|
const url = input.toString();
|
|
5382
5541
|
const method = (init.method ?? 'GET').toUpperCase();
|
|
5383
5542
|
const secretHeaderMarkers = secretAuthHeaderMarkers(init.auth);
|
|
@@ -5406,26 +5565,77 @@ function createMinimalWorkerCtx(
|
|
|
5406
5565
|
...normalizeFetchHeaders(init.headers),
|
|
5407
5566
|
...secretHeaders,
|
|
5408
5567
|
};
|
|
5409
|
-
const
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
allowedOrigins
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5568
|
+
const allowedOrigins = getAllowedWorkerFetchOrigins(req);
|
|
5569
|
+
const useRuntimeEgressFetch = shouldUseRuntimeEgressFetch(
|
|
5570
|
+
url,
|
|
5571
|
+
allowedOrigins,
|
|
5572
|
+
);
|
|
5573
|
+
const egressSlot = shouldPaceWorkerEgressFetch(url, allowedOrigins)
|
|
5574
|
+
? await governor.acquireToolSlot(CTX_FETCH_EGRESS_TOOL_ID, {
|
|
5575
|
+
signal: abortSignal,
|
|
5576
|
+
})
|
|
5577
|
+
: null;
|
|
5578
|
+
try {
|
|
5579
|
+
if (useRuntimeEgressFetch) {
|
|
5580
|
+
// Customer-authored plays are allowed to call arbitrary public HTTP(S)
|
|
5581
|
+
// endpoints. The thing we must not do is let an isolate perform DNS
|
|
5582
|
+
// resolution for attacker-controlled hostnames and then trust only
|
|
5583
|
+
// pre-fetch URL validation: the original pentest issue was exactly
|
|
5584
|
+
// redirect/DNS-rebinding SSRF into private metadata or Deepline
|
|
5585
|
+
// runtime addresses. Cloudflare Worker fetch does not expose a
|
|
5586
|
+
// connect-time DNS validation hook, so arbitrary hostnames go through
|
|
5587
|
+
// the Deepline Node egress broker, which uses safeOutboundFetch for
|
|
5588
|
+
// every connection and redirect.
|
|
5589
|
+
//
|
|
5590
|
+
// The egressSlot is acquired before the broker call: ctx.fetch and
|
|
5591
|
+
// generic_http_request share the same generic_http pacing bucket, so
|
|
5592
|
+
// switching syntax cannot bypass arbitrary-HTTP egress limits.
|
|
5593
|
+
// Direct Worker fetch below is still limited to runtime origins and
|
|
5594
|
+
// IP literals; public IP literals also consume this slot because
|
|
5595
|
+
// they are customer egress.
|
|
5596
|
+
const egressResponse = await postRuntimeEgressFetch(
|
|
5597
|
+
req,
|
|
5598
|
+
{
|
|
5599
|
+
url,
|
|
5600
|
+
method,
|
|
5601
|
+
headers,
|
|
5602
|
+
body: fetchBodyDescriptor(init.body),
|
|
5603
|
+
redirect: init.redirect,
|
|
5604
|
+
sensitiveHeaders: Object.keys(secretHeaderMarkers),
|
|
5605
|
+
},
|
|
5606
|
+
(retryAfterMs) =>
|
|
5607
|
+
governor.reportProviderBackpressure({
|
|
5608
|
+
provider: CTX_FETCH_EGRESS_PROVIDER,
|
|
5609
|
+
retryAfterMs,
|
|
5610
|
+
}),
|
|
5611
|
+
() => governor.chargeBudget('retry'),
|
|
5612
|
+
);
|
|
5613
|
+
assertNotAborted(abortSignal);
|
|
5614
|
+
return secretRedactor.redact(egressResponse);
|
|
5615
|
+
}
|
|
5616
|
+
const fetchInit = { ...init, headers };
|
|
5617
|
+
delete fetchInit.auth;
|
|
5618
|
+
const response = await safeWorkerPublicFetch(url, fetchInit, {
|
|
5619
|
+
allowedOrigins,
|
|
5620
|
+
sensitiveHeaders: Object.keys(secretHeaderMarkers),
|
|
5621
|
+
});
|
|
5622
|
+
assertNotAborted(abortSignal);
|
|
5623
|
+
const bodyText = await response.text();
|
|
5624
|
+
const redactedBodyText = secretRedactor.redactString(bodyText);
|
|
5625
|
+
return {
|
|
5626
|
+
ok: response.ok,
|
|
5627
|
+
status: response.status,
|
|
5628
|
+
statusText: response.statusText,
|
|
5629
|
+
url: response.url,
|
|
5630
|
+
headers: secretRedactor.redact(
|
|
5631
|
+
Object.fromEntries(response.headers.entries()),
|
|
5632
|
+
) as Record<string, string>,
|
|
5633
|
+
bodyText: redactedBodyText,
|
|
5634
|
+
json: secretRedactor.redact(parseFetchJsonOrNull(bodyText)),
|
|
5635
|
+
};
|
|
5636
|
+
} finally {
|
|
5637
|
+
egressSlot?.release();
|
|
5638
|
+
}
|
|
5429
5639
|
});
|
|
5430
5640
|
},
|
|
5431
5641
|
secrets: {
|
|
@@ -172,7 +172,9 @@ export type PlayBindings = {
|
|
|
172
172
|
* Customer-authored play secrets this play is allowed to use at runtime.
|
|
173
173
|
* Values are never bundled or exposed by the SDK; access them with
|
|
174
174
|
* `ctx.secrets.get("NAME")` and approved helpers such as
|
|
175
|
-
* `ctx.secrets.bearer(handle)`.
|
|
175
|
+
* `ctx.secrets.bearer(handle)`. Secret-authenticated `ctx.fetch` calls
|
|
176
|
+
* require an https:// URL so customer secrets never leave Deepline over
|
|
177
|
+
* plaintext HTTP.
|
|
176
178
|
*/
|
|
177
179
|
secrets?: readonly string[];
|
|
178
180
|
};
|
|
@@ -817,6 +819,7 @@ export interface DeeplinePlayRuntimeContext {
|
|
|
817
819
|
* is recorded under `key` so workflow replay sees the same value. Prefer
|
|
818
820
|
* `ctx.tools.execute(...)` for Deepline-managed provider APIs because tools
|
|
819
821
|
* handle auth, retries, rate limits, extraction metadata, and spend tracking.
|
|
822
|
+
* If `init.auth` comes from `ctx.secrets`, `url` must be https://.
|
|
820
823
|
*
|
|
821
824
|
* @param key - Checkpoint id.
|
|
822
825
|
* @param url - URL to fetch.
|
|
@@ -101,10 +101,10 @@ export const SDK_RELEASE = {
|
|
|
101
101
|
// 0.1.108 ships explicit dataset column/tool recompute policy and removes
|
|
102
102
|
// the SDK enrich generator's one-second stale policy.
|
|
103
103
|
// 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
|
|
104
|
-
version: '0.1.
|
|
104
|
+
version: '0.1.135',
|
|
105
105
|
apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
|
|
106
106
|
supportPolicy: {
|
|
107
|
-
latest: '0.1.
|
|
107
|
+
latest: '0.1.135',
|
|
108
108
|
minimumSupported: '0.1.53',
|
|
109
109
|
deprecatedBelow: '0.1.53',
|
|
110
110
|
commandMinimumSupported: [
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PacingRule } from './governor/rate-state-backend';
|
|
2
|
+
|
|
3
|
+
export interface BuiltinPacingPolicy {
|
|
4
|
+
readonly provider: string;
|
|
5
|
+
readonly rules: readonly PacingRule[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared arbitrary-HTTP egress lane.
|
|
10
|
+
*
|
|
11
|
+
* Deepline exposes a first-class `generic_http_request` integration tool, and
|
|
12
|
+
* play authors also have the more ergonomic `ctx.fetch(...)` primitive. Those
|
|
13
|
+
* are two user-facing spellings of the same risk: customer-controlled outbound
|
|
14
|
+
* HTTP to arbitrary public endpoints.
|
|
15
|
+
*
|
|
16
|
+
* Do not create a separate `ctx.fetch` provider bucket. Both surfaces must pace
|
|
17
|
+
* through the existing `generic_http` provider identity so a play cannot bypass
|
|
18
|
+
* generic HTTP limits by switching syntax.
|
|
19
|
+
*/
|
|
20
|
+
export const GENERIC_HTTP_REQUEST_TOOL_ID = 'generic_http_request';
|
|
21
|
+
export const GENERIC_HTTP_PROVIDER = 'generic_http';
|
|
22
|
+
export const GENERIC_HTTP_PROVIDER_SHARED_RULE_ID =
|
|
23
|
+
'generic_http:provider_shared';
|
|
24
|
+
export const GENERIC_HTTP_PROVIDER_SHARED_REQUESTS_PER_SECOND = 25;
|
|
25
|
+
|
|
26
|
+
export const CTX_FETCH_EGRESS_TOOL_ID = GENERIC_HTTP_REQUEST_TOOL_ID;
|
|
27
|
+
export const CTX_FETCH_EGRESS_PROVIDER = GENERIC_HTTP_PROVIDER;
|
|
28
|
+
export const CTX_FETCH_EGRESS_PACING_RULE = {
|
|
29
|
+
ruleId: GENERIC_HTTP_PROVIDER_SHARED_RULE_ID,
|
|
30
|
+
requestsPerWindow: GENERIC_HTTP_PROVIDER_SHARED_REQUESTS_PER_SECOND,
|
|
31
|
+
windowMs: 1000,
|
|
32
|
+
maxConcurrency: null,
|
|
33
|
+
} satisfies PacingRule;
|
|
34
|
+
|
|
35
|
+
export const CTX_FETCH_EGRESS_PACING = {
|
|
36
|
+
provider: CTX_FETCH_EGRESS_PROVIDER,
|
|
37
|
+
rules: [CTX_FETCH_EGRESS_PACING_RULE],
|
|
38
|
+
} satisfies BuiltinPacingPolicy;
|
|
39
|
+
|
|
40
|
+
export function resolveBuiltinPacing(
|
|
41
|
+
toolId: string,
|
|
42
|
+
): { provider: string; rules: PacingRule[] } | null {
|
|
43
|
+
if (toolId !== CTX_FETCH_EGRESS_TOOL_ID) return null;
|
|
44
|
+
return {
|
|
45
|
+
provider: CTX_FETCH_EGRESS_PACING.provider,
|
|
46
|
+
rules: [...CTX_FETCH_EGRESS_PACING.rules],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
type PacingResolver,
|
|
36
36
|
type PlayExecutionGovernor,
|
|
37
37
|
} from './governor/governor';
|
|
38
|
+
import {
|
|
39
|
+
CTX_FETCH_EGRESS_TOOL_ID,
|
|
40
|
+
resolveBuiltinPacing,
|
|
41
|
+
} from './builtin-pacing';
|
|
38
42
|
import { InMemoryRateStateBackend } from './governor/in-memory-rate-state-backend';
|
|
39
43
|
import type { PacingRule } from './governor/rate-state-backend';
|
|
40
44
|
import {
|
|
@@ -95,6 +99,7 @@ import {
|
|
|
95
99
|
} from './secret-redaction';
|
|
96
100
|
import {
|
|
97
101
|
assertNoSecretTaint,
|
|
102
|
+
assertSecretAuthUsesTls,
|
|
98
103
|
createBearerSecretAuth,
|
|
99
104
|
createHeaderSecretAuth,
|
|
100
105
|
createSecretHandle,
|
|
@@ -642,6 +647,8 @@ function createPacingResolver(
|
|
|
642
647
|
getToolQueueHints: ContextOptions['getToolQueueHints'],
|
|
643
648
|
): PacingResolver {
|
|
644
649
|
return async (toolId: string) => {
|
|
650
|
+
const builtin = resolveBuiltinPacing(toolId);
|
|
651
|
+
if (builtin) return builtin;
|
|
645
652
|
const hints = getToolQueueHints ? await getToolQueueHints(toolId) : [];
|
|
646
653
|
if (hints.length === 0) {
|
|
647
654
|
return null;
|
|
@@ -4086,6 +4093,11 @@ export class PlayContextImpl {
|
|
|
4086
4093
|
if (init.auth !== undefined && !isSecretAuth(init.auth)) {
|
|
4087
4094
|
throw new Error('ctx.fetch auth must come from ctx.secrets.');
|
|
4088
4095
|
}
|
|
4096
|
+
// Secret handles are deliberately resolved at the last possible moment, so
|
|
4097
|
+
// plaintext never lands in durable keys, receipts, map rows, or generic tool
|
|
4098
|
+
// payloads. The one place a customer secret is allowed to leave Deepline is
|
|
4099
|
+
// the requested auth header, and that transport must be TLS.
|
|
4100
|
+
assertSecretAuthUsesTls(init.auth, input, 'ctx.fetch');
|
|
4089
4101
|
const secretHeaderMarkers = secretAuthHeaderMarkers(init.auth);
|
|
4090
4102
|
|
|
4091
4103
|
return this.executeWithRuntimeReceipt<PlayFetchResponse>(
|
|
@@ -4147,71 +4159,89 @@ export class PlayContextImpl {
|
|
|
4147
4159
|
}
|
|
4148
4160
|
}
|
|
4149
4161
|
|
|
4162
|
+
// ctx.fetch is arbitrary customer-directed egress. Pace it through
|
|
4163
|
+
// the same generic_http_request lane as the explicit Generic HTTP
|
|
4164
|
+
// integration tool so switching syntax cannot bypass per-org limits.
|
|
4165
|
+
const egressSlot = await this.governor.acquireToolSlot(
|
|
4166
|
+
CTX_FETCH_EGRESS_TOOL_ID,
|
|
4167
|
+
);
|
|
4150
4168
|
let response: Response | null = null;
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
}
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4169
|
+
try {
|
|
4170
|
+
const canRetryTransport = ['GET', 'HEAD', 'OPTIONS'].includes(
|
|
4171
|
+
method,
|
|
4172
|
+
);
|
|
4173
|
+
const { safePublicFetch } = await loadSafeFetch();
|
|
4174
|
+
for (
|
|
4175
|
+
let attempt = 1;
|
|
4176
|
+
attempt <= FETCH_TRANSPORT_MAX_ATTEMPTS;
|
|
4177
|
+
attempt += 1
|
|
4178
|
+
) {
|
|
4179
|
+
try {
|
|
4180
|
+
response = await safePublicFetch(url, fetchInit, {
|
|
4181
|
+
fetchImpl: this.#options.fetchImpl,
|
|
4182
|
+
sensitiveHeaders: Object.keys(secretHeaderMarkers),
|
|
4183
|
+
});
|
|
4184
|
+
break;
|
|
4185
|
+
} catch (error) {
|
|
4186
|
+
if (isUnsafeOutboundUrlError(error)) {
|
|
4187
|
+
throw error;
|
|
4188
|
+
}
|
|
4189
|
+
const message =
|
|
4190
|
+
error instanceof Error ? error.message : String(error);
|
|
4191
|
+
if (
|
|
4192
|
+
canRetryTransport &&
|
|
4193
|
+
attempt < FETCH_TRANSPORT_MAX_ATTEMPTS
|
|
4194
|
+
) {
|
|
4195
|
+
this.log(
|
|
4196
|
+
`ctx.fetch(${method} ${url}) transport failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}; retrying: ${message}`,
|
|
4197
|
+
);
|
|
4198
|
+
await new Promise((resolve) =>
|
|
4199
|
+
setTimeout(
|
|
4200
|
+
resolve,
|
|
4201
|
+
FETCH_TRANSPORT_RETRY_DELAY_MS * attempt,
|
|
4202
|
+
),
|
|
4203
|
+
);
|
|
4204
|
+
continue;
|
|
4205
|
+
}
|
|
4206
|
+
throw new Error(
|
|
4207
|
+
`ctx.fetch(${method} ${url}) failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
|
|
4176
4208
|
);
|
|
4177
|
-
continue;
|
|
4178
4209
|
}
|
|
4210
|
+
}
|
|
4211
|
+
if (!response) {
|
|
4179
4212
|
throw new Error(
|
|
4180
|
-
`ctx.fetch(${method} ${url}) failed
|
|
4213
|
+
`ctx.fetch(${method} ${url}) failed before receiving a response.`,
|
|
4181
4214
|
);
|
|
4182
4215
|
}
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
headers: this.secretRedactor.redact(
|
|
4197
|
-
Object.fromEntries(response.headers.entries()),
|
|
4198
|
-
) as Record<string, string>,
|
|
4199
|
-
bodyText: redactedBodyText,
|
|
4200
|
-
json: this.secretRedactor.redact(parseJsonOrNull(bodyText)),
|
|
4201
|
-
};
|
|
4216
|
+
const bodyText = await response.text();
|
|
4217
|
+
const redactedBodyText = this.secretRedactor.redactString(bodyText);
|
|
4218
|
+
const output: PlayFetchResponse = {
|
|
4219
|
+
ok: response.ok,
|
|
4220
|
+
status: response.status,
|
|
4221
|
+
statusText: response.statusText,
|
|
4222
|
+
url: response.url,
|
|
4223
|
+
headers: this.secretRedactor.redact(
|
|
4224
|
+
Object.fromEntries(response.headers.entries()),
|
|
4225
|
+
) as Record<string, string>,
|
|
4226
|
+
bodyText: redactedBodyText,
|
|
4227
|
+
json: this.secretRedactor.redact(parseJsonOrNull(bodyText)),
|
|
4228
|
+
};
|
|
4202
4229
|
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4230
|
+
this.checkpoint.resolvedBoundaries = {
|
|
4231
|
+
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
4232
|
+
[boundaryId]: {
|
|
4233
|
+
kind: 'fetch',
|
|
4234
|
+
url,
|
|
4235
|
+
method,
|
|
4236
|
+
output,
|
|
4237
|
+
completedAt: Date.now(),
|
|
4238
|
+
},
|
|
4239
|
+
};
|
|
4240
|
+
this.#options.onBatchComplete?.(this.checkpoint);
|
|
4241
|
+
return output;
|
|
4242
|
+
} finally {
|
|
4243
|
+
egressSlot.release();
|
|
4244
|
+
}
|
|
4215
4245
|
},
|
|
4216
4246
|
markSkipped: (output) => {
|
|
4217
4247
|
this.log(
|
|
@@ -94,6 +94,19 @@ export function secretAuthHeaderMarkers(
|
|
|
94
94
|
return { [auth.header.toLowerCase()]: `[secret:${auth.secret.name}]` };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export function assertSecretAuthUsesTls(
|
|
98
|
+
auth: SecretAuth | undefined,
|
|
99
|
+
input: string | URL,
|
|
100
|
+
sink: string,
|
|
101
|
+
): void {
|
|
102
|
+
if (!auth) return;
|
|
103
|
+
const url = input instanceof URL ? input : new URL(input);
|
|
104
|
+
if (url.protocol === 'https:') return;
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${sink} with ctx.secrets auth requires an https:// URL. Customer secrets may only leave Deepline over TLS.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
export function assertNoSecretTaint(value: unknown, sink: string): void {
|
|
98
111
|
if (valueContainsSecret(value)) {
|
|
99
112
|
throw new Error(
|
|
@@ -15,6 +15,7 @@ type NodeSafeFetchOptions = {
|
|
|
15
15
|
maxRedirects?: number;
|
|
16
16
|
maxResponseBytes?: number;
|
|
17
17
|
sensitiveHeaders?: Iterable<string>;
|
|
18
|
+
validateUrl?: (url: URL) => void;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
type PreparedBody = {
|
|
@@ -24,6 +25,24 @@ type PreparedBody = {
|
|
|
24
25
|
|
|
25
26
|
export { UnsafeOutboundUrlError };
|
|
26
27
|
|
|
28
|
+
const NULL_BODY_STATUS_CODES = new Set([204, 205, 304]);
|
|
29
|
+
const TRANSPORT_OWNED_REQUEST_HEADERS = new Set([
|
|
30
|
+
'connection',
|
|
31
|
+
'content-length',
|
|
32
|
+
'host',
|
|
33
|
+
'keep-alive',
|
|
34
|
+
'proxy-authenticate',
|
|
35
|
+
'proxy-authorization',
|
|
36
|
+
'te',
|
|
37
|
+
'trailer',
|
|
38
|
+
'transfer-encoding',
|
|
39
|
+
'upgrade',
|
|
40
|
+
'forwarded',
|
|
41
|
+
'x-forwarded-for',
|
|
42
|
+
'x-forwarded-host',
|
|
43
|
+
'x-forwarded-proto',
|
|
44
|
+
]);
|
|
45
|
+
|
|
27
46
|
function cloneHeaders(headers: RequestInit['headers']): Headers {
|
|
28
47
|
return new Headers(headers);
|
|
29
48
|
}
|
|
@@ -32,14 +51,34 @@ function deleteHeader(headers: Headers, name: string) {
|
|
|
32
51
|
if (headers.has(name)) headers.delete(name);
|
|
33
52
|
}
|
|
34
53
|
|
|
54
|
+
function removeTransportOwnedHeaders(headers: Headers): void {
|
|
55
|
+
for (const header of TRANSPORT_OWNED_REQUEST_HEADERS) {
|
|
56
|
+
deleteHeader(headers, header);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function withContentLength(
|
|
61
|
+
body: Buffer | string | undefined,
|
|
62
|
+
headers: Headers,
|
|
63
|
+
): PreparedBody {
|
|
64
|
+
removeTransportOwnedHeaders(headers);
|
|
65
|
+
if (body !== undefined) {
|
|
66
|
+
headers.set(
|
|
67
|
+
'content-length',
|
|
68
|
+
String(typeof body === 'string' ? Buffer.byteLength(body) : body.length),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return { body, headers };
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
async function prepareBody(init: RequestInit): Promise<PreparedBody> {
|
|
36
75
|
const headers = cloneHeaders(init.headers);
|
|
37
76
|
const body = init.body;
|
|
38
77
|
if (body === undefined || body === null) {
|
|
39
|
-
return
|
|
78
|
+
return withContentLength(undefined, headers);
|
|
40
79
|
}
|
|
41
80
|
if (typeof body === 'string') {
|
|
42
|
-
return
|
|
81
|
+
return withContentLength(body, headers);
|
|
43
82
|
}
|
|
44
83
|
if (body instanceof URLSearchParams) {
|
|
45
84
|
if (!headers.has('content-type')) {
|
|
@@ -48,19 +87,19 @@ async function prepareBody(init: RequestInit): Promise<PreparedBody> {
|
|
|
48
87
|
'application/x-www-form-urlencoded;charset=UTF-8',
|
|
49
88
|
);
|
|
50
89
|
}
|
|
51
|
-
return
|
|
90
|
+
return withContentLength(body.toString(), headers);
|
|
52
91
|
}
|
|
53
92
|
if (body instanceof ArrayBuffer) {
|
|
54
|
-
return
|
|
93
|
+
return withContentLength(Buffer.from(body), headers);
|
|
55
94
|
}
|
|
56
95
|
if (ArrayBuffer.isView(body)) {
|
|
57
|
-
return
|
|
58
|
-
|
|
96
|
+
return withContentLength(
|
|
97
|
+
Buffer.from(body.buffer, body.byteOffset, body.byteLength),
|
|
59
98
|
headers,
|
|
60
|
-
|
|
99
|
+
);
|
|
61
100
|
}
|
|
62
101
|
if (typeof Blob !== 'undefined' && body instanceof Blob) {
|
|
63
|
-
return
|
|
102
|
+
return withContentLength(Buffer.from(await body.arrayBuffer()), headers);
|
|
64
103
|
}
|
|
65
104
|
|
|
66
105
|
throw new TypeError(
|
|
@@ -80,6 +119,10 @@ function validateResolvedAddress(address: string) {
|
|
|
80
119
|
}
|
|
81
120
|
}
|
|
82
121
|
|
|
122
|
+
function responseMustNotHaveBody(method: string, status: number): boolean {
|
|
123
|
+
return method === 'HEAD' || NULL_BODY_STATUS_CODES.has(status);
|
|
124
|
+
}
|
|
125
|
+
|
|
83
126
|
const safeLookup: LookupFunction = (hostname, options, callback) => {
|
|
84
127
|
const lookupOptions: dns.LookupAllOptions = {
|
|
85
128
|
all: true,
|
|
@@ -129,27 +172,29 @@ function createRequest(
|
|
|
129
172
|
): Promise<Response> {
|
|
130
173
|
return new Promise((resolve, reject) => {
|
|
131
174
|
const transport = url.protocol === 'https:' ? https : http;
|
|
175
|
+
const method = String(init.method ?? 'GET').toUpperCase();
|
|
132
176
|
const request = transport.request(
|
|
133
177
|
url,
|
|
134
178
|
{
|
|
135
|
-
method
|
|
179
|
+
method,
|
|
136
180
|
headers: headersToRecord(prepared.headers),
|
|
137
181
|
agent: false,
|
|
138
182
|
lookup: safeLookup,
|
|
139
183
|
signal: init.signal ?? undefined,
|
|
140
184
|
},
|
|
141
185
|
(response) => {
|
|
186
|
+
const status = response.statusCode ?? 0;
|
|
187
|
+
const noBodyResponse = responseMustNotHaveBody(method, status);
|
|
142
188
|
const contentLength = Number(response.headers['content-length']);
|
|
143
189
|
if (
|
|
190
|
+
!noBodyResponse &&
|
|
144
191
|
maxResponseBytes !== undefined &&
|
|
145
192
|
Number.isFinite(contentLength) &&
|
|
146
193
|
contentLength > maxResponseBytes
|
|
147
194
|
) {
|
|
148
195
|
response.resume();
|
|
149
196
|
reject(
|
|
150
|
-
new Error(
|
|
151
|
-
`Response body exceeds ${maxResponseBytes} byte limit.`,
|
|
152
|
-
),
|
|
197
|
+
new Error(`Response body exceeds ${maxResponseBytes} byte limit.`),
|
|
153
198
|
);
|
|
154
199
|
return;
|
|
155
200
|
}
|
|
@@ -170,16 +215,21 @@ function createRequest(
|
|
|
170
215
|
);
|
|
171
216
|
return;
|
|
172
217
|
}
|
|
173
|
-
|
|
218
|
+
if (!noBodyResponse) {
|
|
219
|
+
chunks.push(buffer);
|
|
220
|
+
}
|
|
174
221
|
});
|
|
175
222
|
response.on('error', reject);
|
|
176
223
|
response.on('end', () => {
|
|
177
224
|
resolve(
|
|
178
|
-
new Response(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
225
|
+
new Response(
|
|
226
|
+
noBodyResponse ? null : Buffer.concat(chunks),
|
|
227
|
+
{
|
|
228
|
+
status,
|
|
229
|
+
statusText: response.statusMessage,
|
|
230
|
+
headers: response.headers as HeadersInit,
|
|
231
|
+
},
|
|
232
|
+
),
|
|
183
233
|
);
|
|
184
234
|
});
|
|
185
235
|
},
|
|
@@ -241,6 +291,7 @@ export async function safeOutboundFetch(
|
|
|
241
291
|
const maxRedirects = options.maxRedirects ?? 10;
|
|
242
292
|
const maxResponseBytes = options.maxResponseBytes;
|
|
243
293
|
const sensitiveHeaders = options.sensitiveHeaders ?? [];
|
|
294
|
+
const validateUrl = options.validateUrl;
|
|
244
295
|
let currentUrl = assertPublicHttpUrl(input);
|
|
245
296
|
let currentInit: RequestInit = { ...init, redirect: 'manual' };
|
|
246
297
|
|
|
@@ -250,6 +301,7 @@ export async function safeOutboundFetch(
|
|
|
250
301
|
redirectCount += 1
|
|
251
302
|
) {
|
|
252
303
|
const hostname = normalizeUrlHostname(currentUrl.hostname);
|
|
304
|
+
validateUrl?.(currentUrl);
|
|
253
305
|
if (isBlockedIpAddress(hostname)) {
|
|
254
306
|
throw new UnsafeOutboundUrlError(
|
|
255
307
|
`Target host "${hostname}" is not allowed.`,
|
package/dist/cli/index.js
CHANGED
|
@@ -413,10 +413,10 @@ var SDK_RELEASE = {
|
|
|
413
413
|
// 0.1.108 ships explicit dataset column/tool recompute policy and removes
|
|
414
414
|
// the SDK enrich generator's one-second stale policy.
|
|
415
415
|
// 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
|
|
416
|
-
version: "0.1.
|
|
416
|
+
version: "0.1.135",
|
|
417
417
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
418
418
|
supportPolicy: {
|
|
419
|
-
latest: "0.1.
|
|
419
|
+
latest: "0.1.135",
|
|
420
420
|
minimumSupported: "0.1.53",
|
|
421
421
|
deprecatedBelow: "0.1.53",
|
|
422
422
|
commandMinimumSupported: [
|
|
@@ -18916,38 +18916,46 @@ function renderSecret(secret) {
|
|
|
18916
18916
|
const scope = secret.scope === "play" && secret.playName ? `play:${secret.playName}` : secret.scope;
|
|
18917
18917
|
return `${secret.name} (${scope}) - ${secret.status}${secret.hasValue ? ", set" : ", empty"}`;
|
|
18918
18918
|
}
|
|
18919
|
-
async function readHiddenLine(prompt) {
|
|
18920
|
-
|
|
18919
|
+
async function readHiddenLine(prompt, streams = {}) {
|
|
18920
|
+
const inputStream = streams.input ?? import_node_process.stdin;
|
|
18921
|
+
const outputStream = streams.output ?? import_node_process.stdout;
|
|
18922
|
+
if (!inputStream.isTTY || !outputStream.isTTY) {
|
|
18921
18923
|
throw new Error(
|
|
18922
18924
|
"Secret values must be entered from an interactive TTY. Do not pipe, pass, or script secret values."
|
|
18923
18925
|
);
|
|
18924
18926
|
}
|
|
18925
|
-
|
|
18926
|
-
const previousRawMode =
|
|
18927
|
-
|
|
18927
|
+
outputStream.write(prompt);
|
|
18928
|
+
const previousRawMode = inputStream.isRaw;
|
|
18929
|
+
const wasPaused = typeof inputStream.isPaused === "function" ? inputStream.isPaused() : false;
|
|
18930
|
+
if (typeof inputStream.setRawMode === "function") {
|
|
18931
|
+
inputStream.setRawMode(true);
|
|
18932
|
+
}
|
|
18928
18933
|
let value = "";
|
|
18929
|
-
|
|
18934
|
+
inputStream.resume();
|
|
18930
18935
|
return await new Promise((resolve13, reject) => {
|
|
18931
18936
|
let settled = false;
|
|
18932
18937
|
const cleanup = () => {
|
|
18933
|
-
|
|
18934
|
-
|
|
18935
|
-
|
|
18936
|
-
if (typeof
|
|
18937
|
-
|
|
18938
|
+
inputStream.off("data", onData);
|
|
18939
|
+
inputStream.off("end", onEnd);
|
|
18940
|
+
inputStream.off("error", onError);
|
|
18941
|
+
if (typeof inputStream.setRawMode === "function") {
|
|
18942
|
+
inputStream.setRawMode(previousRawMode);
|
|
18943
|
+
}
|
|
18944
|
+
if (wasPaused) {
|
|
18945
|
+
inputStream.pause();
|
|
18938
18946
|
}
|
|
18939
18947
|
};
|
|
18940
18948
|
const finish = (line) => {
|
|
18941
18949
|
if (settled) return;
|
|
18942
18950
|
settled = true;
|
|
18943
|
-
|
|
18951
|
+
outputStream.write("\n");
|
|
18944
18952
|
cleanup();
|
|
18945
18953
|
resolve13(line);
|
|
18946
18954
|
};
|
|
18947
18955
|
const fail = (error) => {
|
|
18948
18956
|
if (settled) return;
|
|
18949
18957
|
settled = true;
|
|
18950
|
-
|
|
18958
|
+
outputStream.write("\n");
|
|
18951
18959
|
cleanup();
|
|
18952
18960
|
reject(error);
|
|
18953
18961
|
};
|
|
@@ -18979,9 +18987,9 @@ async function readHiddenLine(prompt) {
|
|
|
18979
18987
|
};
|
|
18980
18988
|
const onEnd = () => fail(new Error("Secret input ended before a value was entered."));
|
|
18981
18989
|
const onError = (error) => fail(error);
|
|
18982
|
-
|
|
18983
|
-
|
|
18984
|
-
|
|
18990
|
+
inputStream.on("data", onData);
|
|
18991
|
+
inputStream.once("end", onEnd);
|
|
18992
|
+
inputStream.once("error", onError);
|
|
18985
18993
|
if (hiddenInputBuffer) {
|
|
18986
18994
|
const buffered = hiddenInputBuffer;
|
|
18987
18995
|
hiddenInputBuffer = "";
|
package/dist/cli/index.mjs
CHANGED
|
@@ -390,10 +390,10 @@ var SDK_RELEASE = {
|
|
|
390
390
|
// 0.1.108 ships explicit dataset column/tool recompute policy and removes
|
|
391
391
|
// the SDK enrich generator's one-second stale policy.
|
|
392
392
|
// 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
|
|
393
|
-
version: "0.1.
|
|
393
|
+
version: "0.1.135",
|
|
394
394
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
395
395
|
supportPolicy: {
|
|
396
|
-
latest: "0.1.
|
|
396
|
+
latest: "0.1.135",
|
|
397
397
|
minimumSupported: "0.1.53",
|
|
398
398
|
deprecatedBelow: "0.1.53",
|
|
399
399
|
commandMinimumSupported: [
|
|
@@ -18932,38 +18932,46 @@ function renderSecret(secret) {
|
|
|
18932
18932
|
const scope = secret.scope === "play" && secret.playName ? `play:${secret.playName}` : secret.scope;
|
|
18933
18933
|
return `${secret.name} (${scope}) - ${secret.status}${secret.hasValue ? ", set" : ", empty"}`;
|
|
18934
18934
|
}
|
|
18935
|
-
async function readHiddenLine(prompt) {
|
|
18936
|
-
|
|
18935
|
+
async function readHiddenLine(prompt, streams = {}) {
|
|
18936
|
+
const inputStream = streams.input ?? input;
|
|
18937
|
+
const outputStream = streams.output ?? output;
|
|
18938
|
+
if (!inputStream.isTTY || !outputStream.isTTY) {
|
|
18937
18939
|
throw new Error(
|
|
18938
18940
|
"Secret values must be entered from an interactive TTY. Do not pipe, pass, or script secret values."
|
|
18939
18941
|
);
|
|
18940
18942
|
}
|
|
18941
|
-
|
|
18942
|
-
const previousRawMode =
|
|
18943
|
-
|
|
18943
|
+
outputStream.write(prompt);
|
|
18944
|
+
const previousRawMode = inputStream.isRaw;
|
|
18945
|
+
const wasPaused = typeof inputStream.isPaused === "function" ? inputStream.isPaused() : false;
|
|
18946
|
+
if (typeof inputStream.setRawMode === "function") {
|
|
18947
|
+
inputStream.setRawMode(true);
|
|
18948
|
+
}
|
|
18944
18949
|
let value = "";
|
|
18945
|
-
|
|
18950
|
+
inputStream.resume();
|
|
18946
18951
|
return await new Promise((resolve13, reject) => {
|
|
18947
18952
|
let settled = false;
|
|
18948
18953
|
const cleanup = () => {
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
if (typeof
|
|
18953
|
-
|
|
18954
|
+
inputStream.off("data", onData);
|
|
18955
|
+
inputStream.off("end", onEnd);
|
|
18956
|
+
inputStream.off("error", onError);
|
|
18957
|
+
if (typeof inputStream.setRawMode === "function") {
|
|
18958
|
+
inputStream.setRawMode(previousRawMode);
|
|
18959
|
+
}
|
|
18960
|
+
if (wasPaused) {
|
|
18961
|
+
inputStream.pause();
|
|
18954
18962
|
}
|
|
18955
18963
|
};
|
|
18956
18964
|
const finish = (line) => {
|
|
18957
18965
|
if (settled) return;
|
|
18958
18966
|
settled = true;
|
|
18959
|
-
|
|
18967
|
+
outputStream.write("\n");
|
|
18960
18968
|
cleanup();
|
|
18961
18969
|
resolve13(line);
|
|
18962
18970
|
};
|
|
18963
18971
|
const fail = (error) => {
|
|
18964
18972
|
if (settled) return;
|
|
18965
18973
|
settled = true;
|
|
18966
|
-
|
|
18974
|
+
outputStream.write("\n");
|
|
18967
18975
|
cleanup();
|
|
18968
18976
|
reject(error);
|
|
18969
18977
|
};
|
|
@@ -18995,9 +19003,9 @@ async function readHiddenLine(prompt) {
|
|
|
18995
19003
|
};
|
|
18996
19004
|
const onEnd = () => fail(new Error("Secret input ended before a value was entered."));
|
|
18997
19005
|
const onError = (error) => fail(error);
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19006
|
+
inputStream.on("data", onData);
|
|
19007
|
+
inputStream.once("end", onEnd);
|
|
19008
|
+
inputStream.once("error", onError);
|
|
19001
19009
|
if (hiddenInputBuffer) {
|
|
19002
19010
|
const buffered = hiddenInputBuffer;
|
|
19003
19011
|
hiddenInputBuffer = "";
|
package/dist/index.d.mts
CHANGED
|
@@ -2788,7 +2788,9 @@ type PlayBindings = {
|
|
|
2788
2788
|
* Customer-authored play secrets this play is allowed to use at runtime.
|
|
2789
2789
|
* Values are never bundled or exposed by the SDK; access them with
|
|
2790
2790
|
* `ctx.secrets.get("NAME")` and approved helpers such as
|
|
2791
|
-
* `ctx.secrets.bearer(handle)`.
|
|
2791
|
+
* `ctx.secrets.bearer(handle)`. Secret-authenticated `ctx.fetch` calls
|
|
2792
|
+
* require an https:// URL so customer secrets never leave Deepline over
|
|
2793
|
+
* plaintext HTTP.
|
|
2792
2794
|
*/
|
|
2793
2795
|
secrets?: readonly string[];
|
|
2794
2796
|
};
|
|
@@ -3288,6 +3290,7 @@ interface DeeplinePlayRuntimeContext {
|
|
|
3288
3290
|
* is recorded under `key` so workflow replay sees the same value. Prefer
|
|
3289
3291
|
* `ctx.tools.execute(...)` for Deepline-managed provider APIs because tools
|
|
3290
3292
|
* handle auth, retries, rate limits, extraction metadata, and spend tracking.
|
|
3293
|
+
* If `init.auth` comes from `ctx.secrets`, `url` must be https://.
|
|
3291
3294
|
*
|
|
3292
3295
|
* @param key - Checkpoint id.
|
|
3293
3296
|
* @param url - URL to fetch.
|
package/dist/index.d.ts
CHANGED
|
@@ -2788,7 +2788,9 @@ type PlayBindings = {
|
|
|
2788
2788
|
* Customer-authored play secrets this play is allowed to use at runtime.
|
|
2789
2789
|
* Values are never bundled or exposed by the SDK; access them with
|
|
2790
2790
|
* `ctx.secrets.get("NAME")` and approved helpers such as
|
|
2791
|
-
* `ctx.secrets.bearer(handle)`.
|
|
2791
|
+
* `ctx.secrets.bearer(handle)`. Secret-authenticated `ctx.fetch` calls
|
|
2792
|
+
* require an https:// URL so customer secrets never leave Deepline over
|
|
2793
|
+
* plaintext HTTP.
|
|
2792
2794
|
*/
|
|
2793
2795
|
secrets?: readonly string[];
|
|
2794
2796
|
};
|
|
@@ -3288,6 +3290,7 @@ interface DeeplinePlayRuntimeContext {
|
|
|
3288
3290
|
* is recorded under `key` so workflow replay sees the same value. Prefer
|
|
3289
3291
|
* `ctx.tools.execute(...)` for Deepline-managed provider APIs because tools
|
|
3290
3292
|
* handle auth, retries, rate limits, extraction metadata, and spend tracking.
|
|
3293
|
+
* If `init.auth` comes from `ctx.secrets`, `url` must be https://.
|
|
3291
3294
|
*
|
|
3292
3295
|
* @param key - Checkpoint id.
|
|
3293
3296
|
* @param url - URL to fetch.
|
package/dist/index.js
CHANGED
|
@@ -284,10 +284,10 @@ var SDK_RELEASE = {
|
|
|
284
284
|
// 0.1.108 ships explicit dataset column/tool recompute policy and removes
|
|
285
285
|
// the SDK enrich generator's one-second stale policy.
|
|
286
286
|
// 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
|
|
287
|
-
version: "0.1.
|
|
287
|
+
version: "0.1.135",
|
|
288
288
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
289
289
|
supportPolicy: {
|
|
290
|
-
latest: "0.1.
|
|
290
|
+
latest: "0.1.135",
|
|
291
291
|
minimumSupported: "0.1.53",
|
|
292
292
|
deprecatedBelow: "0.1.53",
|
|
293
293
|
commandMinimumSupported: [
|
package/dist/index.mjs
CHANGED
|
@@ -206,10 +206,10 @@ var SDK_RELEASE = {
|
|
|
206
206
|
// 0.1.108 ships explicit dataset column/tool recompute policy and removes
|
|
207
207
|
// the SDK enrich generator's one-second stale policy.
|
|
208
208
|
// 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
|
|
209
|
-
version: "0.1.
|
|
209
|
+
version: "0.1.135",
|
|
210
210
|
apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
|
|
211
211
|
supportPolicy: {
|
|
212
|
-
latest: "0.1.
|
|
212
|
+
latest: "0.1.135",
|
|
213
213
|
minimumSupported: "0.1.53",
|
|
214
214
|
deprecatedBelow: "0.1.53",
|
|
215
215
|
commandMinimumSupported: [
|