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.
@@ -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
- : /^\/api\/v2\/integrations\/[^/]+\/execute$/.test(path)
492
- ? RUNTIME_API_INTEGRATION_EXECUTE_TIMEOUT_MS
493
- : RUNTIME_API_TIMEOUT_MS;
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 ctx.fetch requires a public IP literal target or Deepline runtime origin. Use a Deepline integration tool for other hostname URLs.',
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 fetchInit = { ...init, headers };
5410
- delete fetchInit.auth;
5411
- const response = await safeWorkerPublicFetch(url, fetchInit, {
5412
- allowedOrigins: getAllowedWorkerFetchOrigins(req),
5413
- sensitiveHeaders: Object.keys(secretHeaderMarkers),
5414
- });
5415
- assertNotAborted(abortSignal);
5416
- const bodyText = await response.text();
5417
- const redactedBodyText = secretRedactor.redactString(bodyText);
5418
- return {
5419
- ok: response.ok,
5420
- status: response.status,
5421
- statusText: response.statusText,
5422
- url: response.url,
5423
- headers: secretRedactor.redact(
5424
- Object.fromEntries(response.headers.entries()),
5425
- ) as Record<string, string>,
5426
- bodyText: redactedBodyText,
5427
- json: secretRedactor.redact(parseFetchJsonOrNull(bodyText)),
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.133',
104
+ version: '0.1.135',
105
105
  apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
106
106
  supportPolicy: {
107
- latest: '0.1.133',
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
- const canRetryTransport = ['GET', 'HEAD', 'OPTIONS'].includes(method);
4152
- const { safePublicFetch } = await loadSafeFetch();
4153
- for (
4154
- let attempt = 1;
4155
- attempt <= FETCH_TRANSPORT_MAX_ATTEMPTS;
4156
- attempt += 1
4157
- ) {
4158
- try {
4159
- response = await safePublicFetch(url, fetchInit, {
4160
- fetchImpl: this.#options.fetchImpl,
4161
- sensitiveHeaders: Object.keys(secretHeaderMarkers),
4162
- });
4163
- break;
4164
- } catch (error) {
4165
- if (isUnsafeOutboundUrlError(error)) {
4166
- throw error;
4167
- }
4168
- const message =
4169
- error instanceof Error ? error.message : String(error);
4170
- if (canRetryTransport && attempt < FETCH_TRANSPORT_MAX_ATTEMPTS) {
4171
- this.log(
4172
- `ctx.fetch(${method} ${url}) transport failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}; retrying: ${message}`,
4173
- );
4174
- await new Promise((resolve) =>
4175
- setTimeout(resolve, FETCH_TRANSPORT_RETRY_DELAY_MS * attempt),
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 on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
4213
+ `ctx.fetch(${method} ${url}) failed before receiving a response.`,
4181
4214
  );
4182
4215
  }
4183
- }
4184
- if (!response) {
4185
- throw new Error(
4186
- `ctx.fetch(${method} ${url}) failed before receiving a response.`,
4187
- );
4188
- }
4189
- const bodyText = await response.text();
4190
- const redactedBodyText = this.secretRedactor.redactString(bodyText);
4191
- const output: PlayFetchResponse = {
4192
- ok: response.ok,
4193
- status: response.status,
4194
- statusText: response.statusText,
4195
- url: response.url,
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
- this.checkpoint.resolvedBoundaries = {
4204
- ...(this.checkpoint.resolvedBoundaries ?? {}),
4205
- [boundaryId]: {
4206
- kind: 'fetch',
4207
- url,
4208
- method,
4209
- output,
4210
- completedAt: Date.now(),
4211
- },
4212
- };
4213
- this.#options.onBatchComplete?.(this.checkpoint);
4214
- return output;
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 { body: undefined, headers };
78
+ return withContentLength(undefined, headers);
40
79
  }
41
80
  if (typeof body === 'string') {
42
- return { body, headers };
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 { body: body.toString(), headers };
90
+ return withContentLength(body.toString(), headers);
52
91
  }
53
92
  if (body instanceof ArrayBuffer) {
54
- return { body: Buffer.from(body), headers };
93
+ return withContentLength(Buffer.from(body), headers);
55
94
  }
56
95
  if (ArrayBuffer.isView(body)) {
57
- return {
58
- body: Buffer.from(body.buffer, body.byteOffset, body.byteLength),
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 { body: Buffer.from(await body.arrayBuffer()), headers };
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: init.method ?? 'GET',
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
- chunks.push(buffer);
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(Buffer.concat(chunks), {
179
- status: response.statusCode ?? 0,
180
- statusText: response.statusMessage,
181
- headers: response.headers as HeadersInit,
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.133",
416
+ version: "0.1.135",
417
417
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
418
418
  supportPolicy: {
419
- latest: "0.1.133",
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
- if (!import_node_process.stdin.isTTY || !import_node_process.stdout.isTTY) {
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
- import_node_process.stdout.write(prompt);
18926
- const previousRawMode = import_node_process.stdin.isRaw;
18927
- if (typeof import_node_process.stdin.setRawMode === "function") import_node_process.stdin.setRawMode(true);
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
- import_node_process.stdin.resume();
18934
+ inputStream.resume();
18930
18935
  return await new Promise((resolve13, reject) => {
18931
18936
  let settled = false;
18932
18937
  const cleanup = () => {
18933
- import_node_process.stdin.off("data", onData);
18934
- import_node_process.stdin.off("end", onEnd);
18935
- import_node_process.stdin.off("error", onError);
18936
- if (typeof import_node_process.stdin.setRawMode === "function") {
18937
- import_node_process.stdin.setRawMode(previousRawMode);
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
- import_node_process.stdout.write("\n");
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
- import_node_process.stdout.write("\n");
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
- import_node_process.stdin.on("data", onData);
18983
- import_node_process.stdin.once("end", onEnd);
18984
- import_node_process.stdin.once("error", onError);
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 = "";
@@ -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.133",
393
+ version: "0.1.135",
394
394
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
395
395
  supportPolicy: {
396
- latest: "0.1.133",
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
- if (!input.isTTY || !output.isTTY) {
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
- output.write(prompt);
18942
- const previousRawMode = input.isRaw;
18943
- if (typeof input.setRawMode === "function") input.setRawMode(true);
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
- input.resume();
18950
+ inputStream.resume();
18946
18951
  return await new Promise((resolve13, reject) => {
18947
18952
  let settled = false;
18948
18953
  const cleanup = () => {
18949
- input.off("data", onData);
18950
- input.off("end", onEnd);
18951
- input.off("error", onError);
18952
- if (typeof input.setRawMode === "function") {
18953
- input.setRawMode(previousRawMode);
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
- output.write("\n");
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
- output.write("\n");
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
- input.on("data", onData);
18999
- input.once("end", onEnd);
19000
- input.once("error", onError);
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.133",
287
+ version: "0.1.135",
288
288
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
289
289
  supportPolicy: {
290
- latest: "0.1.133",
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.133",
209
+ version: "0.1.135",
210
210
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
211
211
  supportPolicy: {
212
- latest: "0.1.133",
212
+ latest: "0.1.135",
213
213
  minimumSupported: "0.1.53",
214
214
  deprecatedBelow: "0.1.53",
215
215
  commandMinimumSupported: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.133",
3
+ "version": "0.1.135",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {