deepline 0.3.50 → 0.3.52

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.
@@ -56,6 +56,7 @@
56
56
 
57
57
  // ——— Client ———
58
58
  export { DeeplineClient } from './client.js';
59
+ export { CtxFetchHttpError } from './play.js';
59
60
  export { RunObserveTransportUnavailableError } from './runs/observe-transport.js';
60
61
  export type {
61
62
  BillingCreditPool,
@@ -80,6 +80,7 @@ import {
80
80
  } from '../../shared_libs/play-runtime/tool-result.js';
81
81
  export { readValue, readList } from '../../shared_libs/play-runtime/tool-result.js';
82
82
  import { createDeferredPlayDataset } from '../../shared_libs/plays/dataset.js';
83
+ export { CtxFetchHttpError } from '../../shared_libs/plays/authoring-contract.js';
83
84
  import {
84
85
  QUERY_RESULT_DATASET_PAGE_SIZE,
85
86
  isCustomerDbDatasetTool,
@@ -199,7 +199,7 @@ export const SDK_RELEASE = {
199
199
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
- version: '0.3.50',
202
+ version: '0.3.52',
203
203
  updateSummary:
204
204
  'Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.',
205
205
  packageCapabilities: {
@@ -178,6 +178,7 @@ import {
178
178
  resolveDurableCallCachePolicy,
179
179
  } from './durable-call-cache';
180
180
  import {
181
+ CtxFetchHttpError,
181
182
  PLAY_AUTHORING_CONTRACT_EDITION,
182
183
  normalizePlayAuthoringCustomerDbStatement,
183
184
  validateOptionalPlayAuthoringField,
@@ -287,6 +288,7 @@ import {
287
288
  isSecretAuthInput,
288
289
  isPlaintextSecretPromise,
289
290
  isSecretHandle,
291
+ isSecretValue,
290
292
  secretAuthEntries,
291
293
  secretAuthHeaderMarkers,
292
294
  valueContainsSecret,
@@ -2864,7 +2866,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2864
2866
  this.secretRedactor.register(value);
2865
2867
  return value;
2866
2868
  }
2867
- return this.resolveSecretValue(secret as SecretValue);
2869
+ if (isSecretHandle(secret) || isSecretValue(secret)) {
2870
+ return this.resolveSecretValue(secret);
2871
+ }
2872
+ throw new Error(
2873
+ 'ctx.secrets auth requires a resolved string or an approved secret value.',
2874
+ );
2868
2875
  }
2869
2876
 
2870
2877
  private async resolveSecretValue(secret: SecretValue): Promise<string> {
@@ -4251,6 +4258,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4251
4258
  ) => T;
4252
4259
  onClaimedResult?: (output: T, receiptKey: string) => T;
4253
4260
  shouldPersistFailure?: (error: unknown) => boolean;
4261
+ transient?: boolean;
4254
4262
  markRunningBeforeExecute?: boolean;
4255
4263
  requiresExecutionLock?: boolean;
4256
4264
  executionLockTtlMs?: number;
@@ -4260,6 +4268,28 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4260
4268
  }) => Promise<T>;
4261
4269
  },
4262
4270
  ): Promise<T> {
4271
+ let releaseExternalCallSlot: (() => void) | null = null;
4272
+ const execute = async (leaseId: string | null): Promise<T> =>
4273
+ await opts.execute({
4274
+ leaseId,
4275
+ retainExternalCallSlot: (release) => {
4276
+ if (releaseExternalCallSlot) {
4277
+ release();
4278
+ throw new Error(
4279
+ `ctx.${operation}(${id}) attempted to retain more than one external-call slot.`,
4280
+ );
4281
+ }
4282
+ releaseExternalCallSlot = release;
4283
+ },
4284
+ });
4285
+ if (opts.transient === true) {
4286
+ try {
4287
+ return await execute(null);
4288
+ } finally {
4289
+ const release = releaseExternalCallSlot as (() => void) | null;
4290
+ release?.();
4291
+ }
4292
+ }
4263
4293
  const stalePolicy = resolveDurableCallCachePolicy(
4264
4294
  opts.staleAfterSeconds,
4265
4295
  operation === 'step'
@@ -4279,7 +4309,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4279
4309
  staleAfterSeconds: stalePolicy.staleAfterSeconds,
4280
4310
  cacheEpochMs: this.durableCallCacheEpochMs,
4281
4311
  });
4282
- let releaseExternalCallSlot: (() => void) | null = null;
4283
4312
  try {
4284
4313
  return await executeWithDurableRuntimeReceipt<T>({
4285
4314
  operation,
@@ -4307,19 +4336,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4307
4336
  toolErrorSchemaVersion: this.currentToolErrorSchemaVersion,
4308
4337
  formatError: (error) => this.formatRuntimeError(error),
4309
4338
  log: (message) => this.log(message),
4310
- execute: ({ leaseId }) =>
4311
- opts.execute({
4312
- leaseId,
4313
- retainExternalCallSlot: (release) => {
4314
- if (releaseExternalCallSlot) {
4315
- release();
4316
- throw new Error(
4317
- `ctx.${operation}(${id}) attempted to retain more than one external-call slot.`,
4318
- );
4319
- }
4320
- releaseExternalCallSlot = release;
4321
- },
4322
- }),
4339
+ execute: ({ leaseId }) => execute(leaseId),
4323
4340
  });
4324
4341
  } finally {
4325
4342
  const release = releaseExternalCallSlot as (() => void) | null;
@@ -10206,9 +10223,31 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10206
10223
  ...secretHeaderMarkers,
10207
10224
  },
10208
10225
  row: rowFetchScope,
10226
+ // Edition 5 changes non-2xx responses from cached values to
10227
+ // non-cacheable errors. Partition those receipts from editions
10228
+ // 1–4 so a republished artifact makes a fresh request instead of
10229
+ // inheriting a legacy error record.
10230
+ ...(this.currentAuthoringContractEdition >= 5
10231
+ ? {
10232
+ authoringContractEdition:
10233
+ this.currentAuthoringContractEdition,
10234
+ }
10235
+ : {}),
10209
10236
  }),
10210
10237
  ),
10211
10238
  staleAfterSeconds: options?.staleAfterSeconds,
10239
+ transient: options?.transient === true,
10240
+ onRecovered: (output) => {
10241
+ if (!output.ok && this.currentAuthoringContractEdition >= 5) {
10242
+ throw new CtxFetchHttpError(output);
10243
+ }
10244
+ return output;
10245
+ },
10246
+ shouldPersistFailure: (error) =>
10247
+ !(
10248
+ error instanceof CtxFetchHttpError &&
10249
+ this.currentAuthoringContractEdition >= 5
10250
+ ),
10212
10251
  execute: async ({ retainExternalCallSlot }) => {
10213
10252
  const method = (init.method ?? 'GET').toUpperCase();
10214
10253
  const secretHeaders = await this.resolveSecretAuth(secretAuth);
@@ -10216,6 +10255,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10216
10255
  ...normalizeFetchHeaders(init.headers),
10217
10256
  ...secretHeaders,
10218
10257
  };
10258
+ if (headers['user-agent'] === undefined) {
10259
+ headers['user-agent'] = 'Deepline Play Runtime';
10260
+ }
10219
10261
  const fetchInit = { ...init, headers };
10220
10262
  delete fetchInit.auth;
10221
10263
  const boundaryId = this.durableBoundaryId(
@@ -10234,7 +10276,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10234
10276
  );
10235
10277
 
10236
10278
  const existing = this.checkpoint.resolvedBoundaries?.[boundaryId];
10237
- if (existing?.kind === 'fetch' && 'output' in existing) {
10279
+ // A transient request deliberately has no durable recovery path, so
10280
+ // its response stays in the running Play rather than a checkpoint.
10281
+ if (
10282
+ options?.transient !== true &&
10283
+ existing?.kind === 'fetch' &&
10284
+ 'output' in existing
10285
+ ) {
10238
10286
  this.log(`ctx.fetch(${url}): recovered response from checkpoint`);
10239
10287
  if (this.durableDirectToolResultsBackedByReceipts) {
10240
10288
  // The outer durable receipt is the replay authority in hosted
@@ -10315,6 +10363,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10315
10363
  fetchImpl: this.#options.fetchImpl,
10316
10364
  sensitiveHeaders: Object.keys(secretHeaderMarkers),
10317
10365
  stripHeadersOnCrossOriginRedirect: true,
10366
+ headersOnCrossOriginRedirect: {
10367
+ 'user-agent': 'Deepline Play Runtime',
10368
+ },
10318
10369
  }),
10319
10370
  });
10320
10371
  bodyText = await readCtxFetchBody({
@@ -10366,6 +10417,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10366
10417
  `ctx.fetch(${method} ${url}) failed while reading the response body.`,
10367
10418
  );
10368
10419
  }
10420
+ const rawJson = parseJsonOrNull(bodyText);
10369
10421
  const redactedBodyText = this.secretRedactor.redactString(bodyText);
10370
10422
  const output: PlayFetchResponse = {
10371
10423
  ok: response.ok,
@@ -10376,12 +10428,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10376
10428
  Object.fromEntries(response.headers.entries()),
10377
10429
  ) as Record<string, string>,
10378
10430
  bodyText: redactedBodyText,
10379
- json: this.secretRedactor.redactKnownSecrets(
10380
- parseJsonOrNull(bodyText),
10381
- ),
10431
+ json: this.secretRedactor.redactKnownSecrets(rawJson),
10382
10432
  };
10383
10433
 
10384
- if (!this.durableDirectToolResultsBackedByReceipts) {
10434
+ // Edition 5 adopts normal fetch semantics: a non-2xx response is
10435
+ // a failed durable operation. Throw before checkpoint/receipt
10436
+ // completion so the failure is never cached. Editions 1–4 retain
10437
+ // their response-record behavior for the same upstream response.
10438
+ if (!output.ok && this.currentAuthoringContractEdition >= 5) {
10439
+ throw new CtxFetchHttpError(output);
10440
+ }
10441
+
10442
+ if (
10443
+ options?.transient !== true &&
10444
+ !this.durableDirectToolResultsBackedByReceipts
10445
+ ) {
10385
10446
  this.checkpoint.resolvedBoundaries = {
10386
10447
  ...(this.checkpoint.resolvedBoundaries ?? {}),
10387
10448
  [boundaryId]: {
@@ -697,26 +697,39 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
697
697
  isInFlightRuntimeReceipt(receipt) &&
698
698
  typeof receipt.runId === 'string' &&
699
699
  receipt.runId.trim() === input.runId;
700
- const waitForRunningReceipt = async (): Promise<{
701
- kind: 'recovered';
702
- output: T;
703
- }> => ({
704
- kind: 'recovered',
705
- output: await recoverCompletedReceipt(
706
- await waitForCompletedRuntimeReceipt({
707
- receiptKey: input.receiptKey,
708
- store: input.store,
709
- maxAttempts: input.runningReceiptWaitMaxAttempts,
710
- delayMs: input.runningReceiptWaitDelayMs,
711
- toolErrorSchemaVersion: input.toolErrorSchemaVersion,
712
- }),
713
- 'in_flight',
714
- ),
715
- });
716
- const waitForRunningReceiptOrTimeout = async (): Promise<{
717
- kind: 'recovered';
718
- output: T;
719
- }> => waitForRunningReceipt();
700
+ const waitForRunningReceiptOrRelease = async (): Promise<
701
+ { kind: 'recovered'; output: T } | { kind: 'claimed' }
702
+ > => {
703
+ const maxAttempts =
704
+ input.runningReceiptWaitMaxAttempts ?? DURABLE_RECEIPT_WAIT_MAX_ATTEMPTS;
705
+ const delayMs =
706
+ input.runningReceiptWaitDelayMs ?? DURABLE_RECEIPT_WAIT_DELAY_MS;
707
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
708
+ if (attempt > 0) await sleepReceiptWait(delayMs);
709
+ const current = await input.store.get(input.receiptKey);
710
+ if (current?.status === 'completed' || current?.status === 'skipped') {
711
+ return {
712
+ kind: 'recovered',
713
+ output: await recoverCompletedReceipt(current, 'in_flight'),
714
+ };
715
+ }
716
+ if (current?.status === 'failed') {
717
+ throw runtimeReceiptFailureError(
718
+ current,
719
+ `ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused`,
720
+ input.toolErrorSchemaVersion,
721
+ );
722
+ }
723
+ if (
724
+ (current?.status === 'queued' || current?.status === 'pending') &&
725
+ !current.leaseId &&
726
+ !current.leaseExpiresAt
727
+ ) {
728
+ return await reclaimReceipt();
729
+ }
730
+ }
731
+ throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
732
+ };
720
733
  const repairOrWaitForRunningReceipt = async (
721
734
  receipt: RuntimeStepReceipt,
722
735
  ): Promise<{ kind: 'recovered'; output: T } | { kind: 'claimed' }> => {
@@ -726,7 +739,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
726
739
  return { kind: 'claimed' };
727
740
  }
728
741
  try {
729
- return await waitForRunningReceiptOrTimeout();
742
+ return await waitForRunningReceiptOrRelease();
730
743
  } catch (error) {
731
744
  if (error instanceof RuntimeReceiptWaitTimeoutError) {
732
745
  const recovered = await reclaimReceipt();
@@ -840,6 +853,24 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
840
853
  assertNoSecretTaint(result, `ctx.${input.operation} result`);
841
854
  } catch (error) {
842
855
  if (input.shouldPersistFailure?.(error) === false) {
856
+ // This error is deliberately not a durable execution result (for
857
+ // example, an HTTP non-2xx response under the current ctx.fetch
858
+ // contract). Release only the lease we still own so a later run can
859
+ // make a fresh, idempotency-protected attempt rather than inheriting a
860
+ // running or failed receipt.
861
+ const released = await input.store.release(
862
+ input.receiptKey,
863
+ input.runId,
864
+ ownedLeaseId,
865
+ );
866
+ if (
867
+ !released ||
868
+ (released.status !== 'queued' && released.status !== 'pending')
869
+ ) {
870
+ throw new Error(
871
+ `ctx.${input.operation}(${input.id}): non-cacheable execution failed but receipt ownership could not be released: ${input.formatError(error)}.`,
872
+ );
873
+ }
843
874
  throw error;
844
875
  }
845
876
  // The ownership is uncertain, so neither `fail` nor `release` is safe.
@@ -129,7 +129,11 @@ export function valueContainsSecret(value: unknown): boolean {
129
129
  const seen = new WeakSet<object>();
130
130
  while (pending.length > 0) {
131
131
  const candidate = pending.pop();
132
- if (isSecretValue(candidate) || isSecretAuth(candidate)) return true;
132
+ if (
133
+ isSecretValue(candidate) || isSecretAuth(candidate)
134
+ ) {
135
+ return true;
136
+ }
133
137
  if (typeof candidate === 'string') {
134
138
  if (SECRET_HANDLE_MARKER_RE.test(candidate)) return true;
135
139
  continue;
@@ -7,9 +7,11 @@ import type { ToolExecutionErrorSchemaVersion } from './tool-execution-error';
7
7
  import type { PlayDataset, PlayDatasetInput, PlayDatasetRow } from './dataset';
8
8
 
9
9
  export const LEGACY_PLAY_AUTHORING_CONTRACT_EDITION = 1 as const;
10
- export const PLAY_AUTHORING_CONTRACT_EDITION = 4 as const;
10
+ export const PLAY_AUTHORING_CONTRACT_EDITION = 5 as const;
11
11
  export const PLAY_AUTHORING_INPUT_SCHEMA_SNAPSHOT_EDITION = 3 as const;
12
- export const SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS = [1, 2, 3, 4] as const;
12
+ export const SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS = [
13
+ 1, 2, 3, 4, 5,
14
+ ] as const;
13
15
 
14
16
  export type PlayAuthoringContractEdition =
15
17
  (typeof SUPPORTED_PLAY_AUTHORING_CONTRACT_EDITIONS)[number];
@@ -49,6 +51,47 @@ export const PLAY_AUTHORING_CONTRACT_CHANGELOG = [
49
51
  newWritesEnd: null,
50
52
  readerRemoval: null,
51
53
  },
54
+ {
55
+ edition: 5,
56
+ changed:
57
+ 'ctx.fetch throws a typed, redacted CtxFetchHttpError for non-2xx responses. Editions 1–4 continue to return the response record for every HTTP status.',
58
+ compatibilityOwner: 'Plays Runtime',
59
+ newWritesEnd: null,
60
+ readerRemoval: null,
61
+ },
62
+ ] as const;
63
+
64
+ /**
65
+ * Customer-visible runtime capabilities that are not expressible as one
66
+ * `ctx.*` signature. The generated authoring reference renders this inventory
67
+ * directly; add a row here alongside any new supported runtime global or
68
+ * execution-boundary guarantee.
69
+ */
70
+ export const PLAY_AUTHORING_RUNTIME_CAPABILITIES = [
71
+ {
72
+ capability: 'Durable external I/O',
73
+ surface: '`ctx.fetch`, `ctx.tools.execute`, `ctx.step`, `ctx.runPlay`',
74
+ contract:
75
+ 'Use a `ctx.*` primitive for external work. Each primitive owns durable receipt identity and replay; raw I/O in an authored handler does not.',
76
+ },
77
+ {
78
+ capability: 'Web Crypto',
79
+ surface: '`crypto.subtle`',
80
+ contract:
81
+ 'WebCrypto is available for standards-based signing, verification, encryption, and key import. Keep private material in `ctx.secrets`; `docs-examples/sdk-v2/github-app-jwt.play.ts` signs an RS256 GitHub App JWT and uses `staleAfterSeconds: 0` for its time-varying auth exchange.',
82
+ },
83
+ {
84
+ capability: 'Workspace secrets',
85
+ surface: '`ctx.secrets.get`, `.bearer`, `.header`',
86
+ contract:
87
+ 'Declare the secret at the Play boundary, read it only at runtime, and never return or log it. Secret-bearing HTTP requests require HTTPS.',
88
+ },
89
+ {
90
+ capability: 'Freshness',
91
+ surface: '`staleAfterSeconds` on durable calls',
92
+ contract:
93
+ 'This is call-receipt freshness: omitted/null reuses forever, zero always executes, and a positive integer is a TTL in seconds. It never changes dataset or request identity.',
94
+ },
52
95
  ] as const;
53
96
 
54
97
  export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
@@ -78,6 +121,7 @@ export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
78
121
  'play_authoring_fetch_secret_requires_tls',
79
122
  'play_authoring_fetch_idempotency_required',
80
123
  'play_authoring_fetch_key_reused_in_loop',
124
+ 'play_authoring_fetch_http_error_branch',
81
125
  'play_authoring_binding_invalid',
82
126
  'play_authoring_input_schema_unresolved',
83
127
  'tool_response_raw_access',
@@ -842,12 +886,24 @@ export type PlayAuthoringRuntimeStepOptions = {
842
886
  staleAfterSeconds?: DurableCallStaleAfterSeconds;
843
887
  };
844
888
 
889
+ /**
890
+ * Durable freshness for one `ctx.fetch` call. The key identifies the request;
891
+ * `staleAfterSeconds` controls when that completed request may be reused across
892
+ * Play runs. Omit it (or pass `null`) to reuse indefinitely, pass `0` to
893
+ * execute on every new run, or pass a positive number of seconds for a TTL.
894
+ *
895
+ * Use this for polling or periodically refreshing one external resource. It
896
+ * does not change dataset row identity.
897
+ *
898
+ */
845
899
  export type PlayAuthoringFetchOptions = {
846
900
  staleAfterSeconds?: DurableCallStaleAfterSeconds;
901
+ /** Run through Deepline's guarded outbound path without retaining a response receipt or checkpoint. Use this for short-lived credential exchanges or other response data that should stay in the running Play only. */
902
+ transient?: boolean;
847
903
  };
848
904
 
849
905
  /**
850
- * The value `ctx.fetch(...)` resolves to: a plain durable record, not a WHATWG `Response`. The body is read once at request time so the call can be checkpointed and replayed, so `bodyText` and `json` are already-materialized properties. There is no `.json()`, `.text()`, or `.body` to await — `await res.json()` is a type error, not a typing problem.
906
+ * A durable response record, not a WHATWG `Response`: read the already-materialized `bodyText` and `json` properties; do not call `.json()`, `.text()`, or `.body`.
851
907
  *
852
908
  * @sdkReference runtime 175 PlayFetchResponse
853
909
  */
@@ -870,6 +926,55 @@ export type PlayAuthoringFetchResponse = {
870
926
  json: unknown | null;
871
927
  };
872
928
 
929
+ /**
930
+ * Edition 5+ `ctx.fetch` error for a non-2xx response. Its full readable body is secret-redacted; editions 1–4 retain `PlayFetchResponse { ok: false }`.
931
+ *
932
+ * @sdkReference runtime 178 CtxFetchHttpError
933
+ */
934
+ export class CtxFetchHttpError extends Error {
935
+ /**
936
+ * Play source and the runtime are separately bundled. Recognize the stable
937
+ * public error code so `instanceof` works across that boundary as well.
938
+ */
939
+ static [Symbol.hasInstance](value: unknown): boolean {
940
+ return (
941
+ value !== null &&
942
+ typeof value === 'object' &&
943
+ (value as { code?: unknown }).code === 'CTX_FETCH_HTTP_ERROR'
944
+ );
945
+ }
946
+
947
+ /** Stable machine-readable HTTP-failure code. */
948
+ readonly code = 'CTX_FETCH_HTTP_ERROR' as const;
949
+ /** Upstream HTTP status. */
950
+ readonly status: number;
951
+ /** Upstream HTTP status text. */
952
+ readonly statusText: string;
953
+ /** Final response URL after redirects. */
954
+ readonly url: string;
955
+ /** Secret-redacted response headers. */
956
+ readonly headers: Record<string, string>;
957
+ /** Complete secret-redacted response body. */
958
+ readonly bodyText: string;
959
+ /** Eagerly parsed secret-redacted JSON, or null. */
960
+ readonly json: unknown | null;
961
+
962
+ /** Build the error from the durable response record. */
963
+ constructor(readonly response: PlayAuthoringFetchResponse) {
964
+ const body = response.bodyText || 'empty response body';
965
+ super(
966
+ `ctx.fetch(${response.url}) returned HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}: ${body}`,
967
+ );
968
+ this.name = 'CtxFetchHttpError';
969
+ this.status = response.status;
970
+ this.statusText = response.statusText;
971
+ this.url = response.url;
972
+ this.headers = response.headers;
973
+ this.bodyText = response.bodyText;
974
+ this.json = response.json;
975
+ }
976
+ }
977
+
873
978
  export type PlayAuthoringCustomerDbQueryOptions = {
874
979
  maxRows?: number;
875
980
  timeoutMs?: number;
@@ -1008,7 +1113,8 @@ export interface PlayAuthoringRuntimeContext {
1008
1113
  ): Promise<T>;
1009
1114
 
1010
1115
  /**
1011
- * Execute a durable, replay-safe HTTP request.
1116
+ * Execute a guarded HTTP request. By default it is durable and replay-safe; `staleAfterSeconds` governs only that completed call receipt, never dataset/request identity. Pass `{ transient: true }` for short-lived credential exchanges or other response data that must not be retained in a receipt or checkpoint. Edition 5+ throws `CtxFetchHttpError` for non-2xx; catch it only when the Play intentionally recovers, otherwise let it fail the Play. Editions 1–4 retain their previous response projections.
1117
+ *
1012
1118
  * @sdkReference runtime 170 ctx.fetch(key, url, init)
1013
1119
  */
1014
1120
  fetch(
@@ -2541,8 +2647,9 @@ export const PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
2541
2647
  "export type PlayCallExecution = 'inline';",
2542
2648
  `export type PlayCallOptions = { description: ${cloudReferenceType('ctx.runPlay.options.description')}; execution?: ${cloudReferenceType('ctx.runPlay.options.execution')}; timeoutMs?: ${cloudReferenceType('ctx.runPlay.options.timeoutMs')} };`,
2543
2649
  `export type RuntimeStepOptions = { semanticKey?: ${cloudReferenceType('ctx.step.semanticKey')}; staleAfterSeconds?: ${cloudReferenceType('ctx.step.staleAfterSeconds')} };`,
2544
- `export type FetchOptions = { staleAfterSeconds?: ${cloudReferenceType('ctx.fetch.staleAfterSeconds')} };`,
2650
+ `export type FetchOptions = { staleAfterSeconds?: ${cloudReferenceType('ctx.fetch.staleAfterSeconds')}; transient?: boolean };`,
2545
2651
  'export type PlayFetchResponse = { ok: boolean; status: number; statusText: string; url: string; headers: Record<string, string>; bodyText: string; json: unknown | null };',
2652
+ "export declare class CtxFetchHttpError extends Error { readonly code: 'CTX_FETCH_HTTP_ERROR'; readonly response: PlayFetchResponse; readonly status: number; readonly statusText: string; readonly url: string; readonly headers: Record<string, string>; readonly bodyText: string; readonly json: unknown | null; }",
2546
2653
  'export type StepResolver<Row, Value> = (row: Row, ctx: DeeplinePlayRuntimeContext, index: number, previousCell?: PreviousCell<Value>) => Value | Promise<Value>;',
2547
2654
  'export type DatasetColumnRunInput<Row, Value> = { readonly row: Row; readonly ctx: DeeplinePlayRuntimeContext; readonly index: number; readonly previousCell?: PreviousCell<Value> };',
2548
2655
  'export type DatasetColumnDefinition<Row, Value> = { readonly run: (input: DatasetColumnRunInput<Row, Value>) => Value | Promise<Value>; readonly runIf?: (row: Row, index: number) => boolean | Promise<boolean> };',
@@ -15,6 +15,8 @@ export type SafeFetchOptions = {
15
15
  sensitiveHeaders?: Iterable<string>;
16
16
  /** Drop every caller-supplied header when a redirect changes origin. */
17
17
  stripHeadersOnCrossOriginRedirect?: boolean;
18
+ /** Headers the trusted runtime may add after caller headers are stripped. */
19
+ headersOnCrossOriginRedirect?: HeadersInit;
18
20
  };
19
21
 
20
22
  function removeHeader(headers: Headers, name: string) {
@@ -32,6 +34,7 @@ function requestInitForRedirect(
32
34
  status: number,
33
35
  sensitiveHeaders: Iterable<string>,
34
36
  stripHeadersOnCrossOriginRedirect: boolean,
37
+ headersOnCrossOriginRedirect: HeadersInit | undefined,
35
38
  ): RequestInit {
36
39
  let headers = new Headers(init.headers);
37
40
  let method = String(init.method ?? 'GET').toUpperCase();
@@ -57,7 +60,7 @@ function requestInitForRedirect(
57
60
  // Callers that can construct credentials from plaintext secrets cannot
58
61
  // reliably identify every transformed value. Do not forward any caller
59
62
  // header across an origin boundary in that trust model.
60
- headers = new Headers();
63
+ headers = new Headers(headersOnCrossOriginRedirect);
61
64
  } else {
62
65
  removeHeader(headers, 'authorization');
63
66
  removeHeader(headers, 'cookie');
@@ -88,6 +91,7 @@ export async function safePublicFetch(
88
91
  const sensitiveHeaders = options.sensitiveHeaders ?? [];
89
92
  const stripHeadersOnCrossOriginRedirect =
90
93
  options.stripHeadersOnCrossOriginRedirect === true;
94
+ const headersOnCrossOriginRedirect = options.headersOnCrossOriginRedirect;
91
95
  let currentUrl = assertPublicHttpUrl(input);
92
96
  let currentInit: RequestInit = {
93
97
  ...init,
@@ -135,6 +139,7 @@ export async function safePublicFetch(
135
139
  response.status,
136
140
  sensitiveHeaders,
137
141
  stripHeadersOnCrossOriginRedirect,
142
+ headersOnCrossOriginRedirect,
138
143
  );
139
144
  currentUrl = nextUrl;
140
145
  }