deepline 0.3.7 → 0.3.8

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.
@@ -152,6 +152,8 @@ import type {
152
152
  PlaySecretAuthInput,
153
153
  PlaySecretAwareRequestInit,
154
154
  PlaySecretHandle,
155
+ PlaySecretPromise,
156
+ PlaySecretValue,
155
157
  PlaySqlQuery,
156
158
  PlayReceiptWaitMs,
157
159
  PlayRuntimeTimeoutMs,
@@ -253,6 +255,8 @@ export type SqlListenerEvent<T extends object = Record<string, unknown>> =
253
255
  /** @deprecated Pass a SQL string directly to ctx.customerDb.query. */
254
256
  export type SqlQuery = PlaySqlQuery;
255
257
  export type SecretHandle = PlaySecretHandle;
258
+ export type SecretPromise = PlaySecretPromise;
259
+ export type SecretValue = PlaySecretValue;
256
260
  export type SecretAuth = PlaySecretAuth;
257
261
  export type SecretAuthInput = PlaySecretAuthInput;
258
262
  export type SecretAwareRequestInit = PlaySecretAwareRequestInit;
@@ -192,7 +192,7 @@ export const SDK_RELEASE = {
192
192
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
193
193
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
194
194
  // getters keep their established compatibility behavior.
195
- version: '0.3.7',
195
+ version: '0.3.8',
196
196
  updateSummary:
197
197
  'New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.',
198
198
  contracts: {
@@ -168,6 +168,9 @@ import {
168
168
  validateOptionalPlayAuthoringField,
169
169
  validatePlayAuthoringField,
170
170
  type PlayAuthoringContractEdition,
171
+ type PlaySecretAuth,
172
+ type PlaySecretAwareRequestInit,
173
+ type PlaySecretPromise,
171
174
  type PlaySqlQuery,
172
175
  type PlayAuthoringRunScope,
173
176
  type PlayAuthoringRuntimeContext,
@@ -260,17 +263,24 @@ import type { DocflowObservationUpsert } from './docflow-observation';
260
263
  import {
261
264
  assertNoSecretTaint,
262
265
  assertSecretAuthUsesTls,
266
+ createBase64SecretValue,
263
267
  createBearerSecretAuth,
264
268
  createHeaderSecretAuth,
269
+ createPlaintextSecretPromise,
270
+ createSecretConcat,
265
271
  createSecretHandle,
266
272
  isSecretAuthInput,
273
+ isPlaintextSecretPromise,
274
+ isSecretHandle,
267
275
  secretAuthEntries,
268
276
  secretAuthHeaderMarkers,
269
277
  valueContainsSecret,
270
278
  type SecretAuth,
279
+ type SecretAuthValue,
271
280
  type SecretAuthInput,
272
281
  type SecretAwareRequestInit,
273
282
  type SecretHandle,
283
+ type SecretValue,
274
284
  } from './secret-capability';
275
285
  import type {
276
286
  CsvOptions,
@@ -1218,7 +1228,9 @@ function publicToolResponseForBatchedItem(
1218
1228
  * from callToolAPI. Keep that input contract stable; raw-v2 is attached to the
1219
1229
  * completed logical call separately by publicToolResponseForBatchedItem.
1220
1230
  */
1221
- function legacyResultForBatchSplitter(execution: ParsedToolExecuteResponse): unknown {
1231
+ function legacyResultForBatchSplitter(
1232
+ execution: ParsedToolExecuteResponse,
1233
+ ): unknown {
1222
1234
  if (execution.toolResponse && 'raw' in execution.toolResponse) {
1223
1235
  return execution.toolResponse.raw;
1224
1236
  }
@@ -2260,18 +2272,33 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2260
2272
  };
2261
2273
 
2262
2274
  readonly secrets = {
2263
- get: (name: string): SecretHandle => {
2275
+ get: (name: string): PlaySecretPromise => {
2264
2276
  if (typeof name !== 'string' || !name.trim()) {
2265
2277
  throw new Error('ctx.secrets.get(name) requires a non-empty name.');
2266
2278
  }
2267
- return createSecretHandle(name.trim());
2279
+ const handle = createSecretHandle(name.trim());
2280
+ const value =
2281
+ this.currentAuthoringContractEdition >= 4
2282
+ ? createPlaintextSecretPromise(handle.name, () =>
2283
+ this.resolveSecretHandle(handle),
2284
+ )
2285
+ : handle;
2286
+ return value as unknown as PlaySecretPromise;
2268
2287
  },
2269
- bearer: (secret: SecretHandle): SecretAuth => {
2270
- return createBearerSecretAuth(secret);
2288
+ bearer: (secret: SecretAuthValue): PlaySecretAuth => {
2289
+ return createBearerSecretAuth(secret) as unknown as PlaySecretAuth;
2271
2290
  },
2272
- header: (header: string, secret: SecretHandle): SecretAuth => {
2273
- return createHeaderSecretAuth(header, secret);
2291
+ header: (header: string, secret: SecretAuthValue): PlaySecretAuth => {
2292
+ return createHeaderSecretAuth(
2293
+ header,
2294
+ secret,
2295
+ ) as unknown as PlaySecretAuth;
2274
2296
  },
2297
+ // Only editions 1–3 call these runtime-only compatibility helpers. Edition
2298
+ // 4's public authoring contract intentionally exposes normal strings.
2299
+ concat: (...parts: readonly (string | SecretValue)[]): SecretValue =>
2300
+ createSecretConcat(parts),
2301
+ base64: (value: SecretValue): SecretValue => createBase64SecretValue(value),
2275
2302
  };
2276
2303
 
2277
2304
  constructor(options: ContextOptions) {
@@ -2780,10 +2807,52 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2780
2807
 
2781
2808
  private async resolveSingleSecretAuth(auth: SecretAuth) {
2782
2809
  if (!auth) return {};
2810
+ const value = await this.resolveSecretAuthValue(auth.secret);
2811
+ if (auth.kind === 'bearer') {
2812
+ return { authorization: `Bearer ${value}` };
2813
+ }
2814
+ return { [auth.header.toLowerCase()]: value };
2815
+ }
2816
+
2817
+ private async resolveSecretAuthValue(
2818
+ secret: SecretAuthValue,
2819
+ ): Promise<string> {
2820
+ if (typeof secret === 'string') {
2821
+ this.secretRedactor.register(secret);
2822
+ return secret;
2823
+ }
2824
+ if (isPlaintextSecretPromise(secret)) {
2825
+ const value = await secret;
2826
+ this.secretRedactor.register(value);
2827
+ return value;
2828
+ }
2829
+ return this.resolveSecretValue(secret as SecretValue);
2830
+ }
2831
+
2832
+ private async resolveSecretValue(secret: SecretValue): Promise<string> {
2833
+ if (isSecretHandle(secret)) return this.resolveSecretHandle(secret);
2834
+ const value =
2835
+ secret.kind === 'concat'
2836
+ ? (
2837
+ await Promise.all(
2838
+ secret.parts.map((part) =>
2839
+ typeof part === 'string' ? part : this.resolveSecretValue(part),
2840
+ ),
2841
+ )
2842
+ ).join('')
2843
+ : Buffer.from(
2844
+ await this.resolveSecretValue(secret.value),
2845
+ 'utf8',
2846
+ ).toString('base64');
2847
+ this.secretRedactor.register(value);
2848
+ return value;
2849
+ }
2850
+
2851
+ private async resolveSecretHandle(secret: SecretHandle): Promise<string> {
2783
2852
  let value: string | null = null;
2784
2853
  if (this.#options.resolveSecret) {
2785
2854
  value = await this.#options.resolveSecret({
2786
- name: auth.secret.name,
2855
+ name: secret.name,
2787
2856
  playName: this.#options.playName,
2788
2857
  orgId: this.#options.orgId,
2789
2858
  workflowId: this.#options.workflowId,
@@ -2824,7 +2893,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2824
2893
  },
2825
2894
  body: JSON.stringify({
2826
2895
  action: 'resolve_secret',
2827
- name: auth.secret.name,
2896
+ name: secret.name,
2828
2897
  playName: this.#options.playName,
2829
2898
  }),
2830
2899
  });
@@ -2837,7 +2906,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2837
2906
  this.log(
2838
2907
  `[runtime.secret_resolution_failure] ${JSON.stringify({
2839
2908
  stage: 'transport',
2840
- secret_name: auth.secret.name,
2909
+ secret_name: secret.name,
2841
2910
  gateway_origin: transportGatewayOriginForDiagnostic(url),
2842
2911
  operation_id: operationId,
2843
2912
  request_id: requestId,
@@ -2852,7 +2921,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2852
2921
  );
2853
2922
  if (!retry.retry) {
2854
2923
  throw new Error(
2855
- `Secret ${auth.secret.name} resolution transport failed (request_id=${requestId}): ${diagnostic.message ?? 'unknown transport error'}`,
2924
+ `Secret ${secret.name} resolution transport failed (request_id=${requestId}): ${diagnostic.message ?? 'unknown transport error'}`,
2856
2925
  );
2857
2926
  }
2858
2927
  await waitForSecretResolutionRetry(retry.retryDelayMs);
@@ -2865,7 +2934,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2865
2934
  : NO_SECRET_RESOLUTION_RETRY;
2866
2935
  const responseDiagnostic = {
2867
2936
  stage: response.ok ? 'invalid_response' : 'http_response',
2868
- secret_name: auth.secret.name,
2937
+ secret_name: secret.name,
2869
2938
  gateway_origin: transportGatewayOriginForDiagnostic(url),
2870
2939
  operation_id: operationId,
2871
2940
  request_id: requestId,
@@ -2898,7 +2967,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2898
2967
  continue;
2899
2968
  }
2900
2969
  throw new Error(
2901
- `Secret ${auth.secret.name} is not available to this run (resolution status=${response.status}, request_id=${requestId}).`,
2970
+ `Secret ${secret.name} is not available to this run (resolution status=${response.status}, request_id=${requestId}).`,
2902
2971
  );
2903
2972
  }
2904
2973
 
@@ -2915,7 +2984,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2915
2984
  })}`,
2916
2985
  );
2917
2986
  throw new Error(
2918
- `Secret ${auth.secret.name} is not available to this run (invalid resolution response, request_id=${requestId}).`,
2987
+ `Secret ${secret.name} is not available to this run (invalid resolution response, request_id=${requestId}).`,
2919
2988
  );
2920
2989
  }
2921
2990
 
@@ -2939,13 +3008,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2939
3008
  })}`,
2940
3009
  );
2941
3010
  throw new Error(
2942
- `Secret ${auth.secret.name} is not available to this run (missing resolution value, request_id=${requestId}).`,
3011
+ `Secret ${secret.name} is not available to this run (missing resolution value, request_id=${requestId}).`,
2943
3012
  );
2944
3013
  }
2945
3014
  if (attempt > 1) {
2946
3015
  this.log(
2947
3016
  `[runtime.secret_resolution_recovered] ${JSON.stringify({
2948
- secret_name: auth.secret.name,
3017
+ secret_name: secret.name,
2949
3018
  gateway_origin: transportGatewayOriginForDiagnostic(url),
2950
3019
  operation_id: operationId,
2951
3020
  request_id: requestId,
@@ -2962,15 +3031,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2962
3031
  );
2963
3032
  }
2964
3033
  if (!value) {
2965
- throw new Error(
2966
- `Secret ${auth.secret.name} is not available to this run.`,
2967
- );
3034
+ throw new Error(`Secret ${secret.name} is not available to this run.`);
2968
3035
  }
2969
3036
  this.secretRedactor.register(value);
2970
- if (auth.kind === 'bearer') {
2971
- return { authorization: `Bearer ${value}` };
2972
- }
2973
- return { [auth.header.toLowerCase()]: value };
3037
+ return value;
2974
3038
  }
2975
3039
 
2976
3040
  private setMapFrame(frame: MapExecutionFrame): void {
@@ -9709,7 +9773,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9709
9773
 
9710
9774
  log(msg: string): void {
9711
9775
  assertNoSecretTaint(msg, 'ctx.log');
9712
- const line = `[${new Date().toISOString()}] ${msg}`;
9776
+ const line = `[${new Date().toISOString()}] ${this.secretRedactor.redactRegisteredSecrets(msg)}`;
9713
9777
  this.logBuffer.push(line);
9714
9778
  this.#options.onLog?.(line);
9715
9779
  if (this.#options.verbose) console.log(line);
@@ -9750,7 +9814,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9750
9814
  async fetch(
9751
9815
  key: string,
9752
9816
  input: string | URL,
9753
- init: SecretAwareRequestInit = {},
9817
+ init: PlaySecretAwareRequestInit | SecretAwareRequestInit = {},
9754
9818
  options?: FetchOptions,
9755
9819
  ): Promise<PlayFetchResponse> {
9756
9820
  validatePlayAuthoringField('ctx.fetch.key', key);
@@ -9765,25 +9829,68 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9765
9829
  }
9766
9830
  : null;
9767
9831
 
9768
- if (valueContainsSecret(input) || valueContainsSecret(init.body)) {
9832
+ const url = input.toString();
9833
+ const parsedUrl = new URL(url);
9834
+ const urlContainsResolvedSecret =
9835
+ this.secretRedactor.containsRegisteredSecret(url, {
9836
+ includeEncoded: true,
9837
+ minimumLength: 4,
9838
+ }) ||
9839
+ [...parsedUrl.searchParams.values()].some((value) =>
9840
+ this.secretRedactor.matchesRegisteredSecret(value),
9841
+ );
9842
+ if (valueContainsSecret(input) || urlContainsResolvedSecret) {
9843
+ throw new Error(
9844
+ 'ctx.fetch does not allow secrets in the URL. Use an approved secret auth helper or request body.',
9845
+ );
9846
+ }
9847
+ if (valueContainsSecret(init.body)) {
9769
9848
  throw new Error(
9770
- 'ctx.fetch does not allow secrets in the URL or body. Use an approved secret auth helper.',
9849
+ 'ctx.fetch does not allow opaque secret values in the body. Await ctx.secrets.get(...) before constructing the body.',
9771
9850
  );
9772
9851
  }
9773
- if (valueContainsSecret(init.headers)) {
9852
+ const requestBody = typeof init.body === 'string' ? init.body : null;
9853
+ const bodyContainsResolvedSecret =
9854
+ requestBody !== null &&
9855
+ this.secretRedactor.containsRegisteredSecret(requestBody, {
9856
+ includeEncoded: true,
9857
+ });
9858
+ if (bodyContainsResolvedSecret && parsedUrl.protocol !== 'https:') {
9859
+ throw new Error(
9860
+ 'ctx.fetch with a resolved secret in the request body requires an https:// URL. Customer secrets may only leave Deepline over TLS.',
9861
+ );
9862
+ }
9863
+ const receiptBody = bodyContainsResolvedSecret
9864
+ ? this.secretRedactor.redactRegisteredSecrets(requestBody)
9865
+ : requestBody;
9866
+ const rawHeaders = normalizeFetchHeaders(init.headers);
9867
+ if (
9868
+ valueContainsSecret(init.headers) ||
9869
+ Object.values(rawHeaders).some(
9870
+ (value) =>
9871
+ this.secretRedactor.matchesRegisteredSecret(value) ||
9872
+ this.secretRedactor.containsRegisteredSecret(value, {
9873
+ minimumLength: 4,
9874
+ }),
9875
+ )
9876
+ ) {
9774
9877
  throw new Error(
9775
9878
  'ctx.fetch does not allow raw secret headers. Use ctx.secrets.bearer(...) or ctx.secrets.header(...).',
9776
9879
  );
9777
9880
  }
9778
- if (init.auth !== undefined && !isSecretAuthInput(init.auth)) {
9779
- throw new Error('ctx.fetch auth must come from ctx.secrets.');
9881
+ let secretAuth: SecretAuthInput | undefined;
9882
+ if (init.auth !== undefined) {
9883
+ if (!isSecretAuthInput(init.auth)) {
9884
+ throw new Error('ctx.fetch auth must come from ctx.secrets.');
9885
+ }
9886
+ secretAuth = init.auth;
9780
9887
  }
9781
9888
  // Secret handles are deliberately resolved at the last possible moment, so
9782
9889
  // plaintext never lands in durable keys, receipts, map rows, or generic tool
9783
9890
  // payloads. The one place a customer secret is allowed to leave Deepline is
9784
9891
  // the requested auth header, and that transport must be TLS.
9785
- assertSecretAuthUsesTls(init.auth, input, 'ctx.fetch');
9786
- const secretHeaderMarkers = secretAuthHeaderMarkers(init.auth);
9892
+ assertSecretAuthUsesTls(secretAuth, input, 'ctx.fetch');
9893
+ const secretHeaderMarkers = secretAuthHeaderMarkers(secretAuth);
9787
9894
 
9788
9895
  const execution = this.executeWithRuntimeReceipt<PlayFetchResponse>(
9789
9896
  'fetch',
@@ -9793,8 +9900,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9793
9900
  semanticKey: stableDigest(
9794
9901
  stableStringify({
9795
9902
  method: (init.method ?? 'GET').toUpperCase(),
9796
- url: input.toString(),
9797
- body: typeof init.body === 'string' ? init.body : null,
9903
+ url,
9904
+ body: receiptBody,
9798
9905
  safeHeaders: {
9799
9906
  ...normalizeFetchHeaders(init.headers),
9800
9907
  ...secretHeaderMarkers,
@@ -9804,9 +9911,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9804
9911
  ),
9805
9912
  staleAfterSeconds: options?.staleAfterSeconds,
9806
9913
  execute: async ({ retainExternalCallSlot }) => {
9807
- const url = input.toString();
9808
9914
  const method = (init.method ?? 'GET').toUpperCase();
9809
- const secretHeaders = await this.resolveSecretAuth(init.auth);
9915
+ const secretHeaders = await this.resolveSecretAuth(secretAuth);
9810
9916
  const headers: Record<string, string> = {
9811
9917
  ...normalizeFetchHeaders(init.headers),
9812
9918
  ...secretHeaders,
@@ -9822,7 +9928,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9822
9928
  ...normalizeFetchHeaders(init.headers),
9823
9929
  ...secretHeaderMarkers,
9824
9930
  },
9825
- body: typeof init.body === 'string' ? init.body : null,
9931
+ body: receiptBody,
9826
9932
  row: rowFetchScope,
9827
9933
  }),
9828
9934
  )}`,
@@ -9909,6 +10015,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9909
10015
  safePublicFetch(url, attemptInit, {
9910
10016
  fetchImpl: this.#options.fetchImpl,
9911
10017
  sensitiveHeaders: Object.keys(secretHeaderMarkers),
10018
+ stripHeadersOnCrossOriginRedirect: true,
9912
10019
  }),
9913
10020
  });
9914
10021
  bodyText = await readCtxFetchBody({
@@ -15,7 +15,7 @@ import { DAYTONA_PLAY_RUNNER_LABEL_SOURCE } from './daytona-labels';
15
15
  const DAYTONA_CREATE_TIMEOUT_SECONDS = 10;
16
16
  const DAYTONA_CREATE_RETRY_DELAYS_MS = [0, 500, 1_500] as const;
17
17
  const DAYTONA_TIMED_OUT_CREATE_RECONCILE_DELAYS_MS = [
18
- 0, 250, 750, 1_500, 3_000,
18
+ 0, 1_000, 3_000, 6_000, 12_000,
19
19
  ] as const;
20
20
  // Explicit runner deadline + scheduler GC own the normal lifecycle. Daytona's
21
21
  // inactivity stop is a wider crash backstop measured from sandbox creation, so
@@ -2,10 +2,16 @@ import type {
2
2
  PlaySecretAuth,
3
3
  PlaySecretAwareRequestInit,
4
4
  PlaySecretHandle,
5
+ PlaySecretPromise,
5
6
  } from '../plays/authoring-contract';
7
+ import { sha256Hex } from '../plays/row-identity';
6
8
 
7
9
  const SECRET_HANDLE_BRAND = Symbol.for('deepline.secret.handle');
8
10
  const SECRET_AUTH_BRAND = Symbol.for('deepline.secret.auth');
11
+ const SECRET_VALUE_BRAND = Symbol.for('deepline.secret.value');
12
+ const SECRET_PLAINTEXT_PROMISE_BRAND = Symbol.for(
13
+ 'deepline.secret.plaintext-promise',
14
+ );
9
15
  const SECRET_HANDLE_MARKER_RE = /\[secret:[A-Z0-9_ -]+\]/i;
10
16
 
11
17
  export type SecretHandle = PlaySecretHandle & {
@@ -15,26 +21,61 @@ export type SecretHandle = PlaySecretHandle & {
15
21
  toJSON(): never;
16
22
  };
17
23
 
18
- export type SecretAuth = PlaySecretAuth &
24
+ /**
25
+ * Runtime-only opaque values retained to execute authoring-contract editions
26
+ * 1–3. Edition 4's public Play type deliberately no longer exposes these
27
+ * helpers; published legacy artifacts still invoke them at runtime.
28
+ */
29
+ type SecretExpression =
30
+ | {
31
+ readonly [SECRET_VALUE_BRAND]: true;
32
+ readonly kind: 'concat';
33
+ readonly parts: readonly (string | SecretValue)[];
34
+ toString(): string;
35
+ toJSON(): never;
36
+ }
37
+ | {
38
+ readonly [SECRET_VALUE_BRAND]: true;
39
+ readonly kind: 'base64';
40
+ readonly value: SecretValue;
41
+ toString(): string;
42
+ toJSON(): never;
43
+ };
44
+
45
+ export type SecretValue = SecretHandle | SecretExpression;
46
+ export type PlaintextSecretPromise = PlaySecretPromise & {
47
+ readonly [SECRET_PLAINTEXT_PROMISE_BRAND]: true;
48
+ readonly name: string;
49
+ };
50
+ export type SecretAuthValue =
51
+ | string
52
+ | PlaySecretPromise
53
+ | PlaintextSecretPromise
54
+ | PlaySecretHandle
55
+ | SecretValue;
56
+
57
+ export type SecretAuth = Omit<PlaySecretAuth, 'secret'> &
19
58
  (
20
59
  | {
21
60
  readonly [SECRET_AUTH_BRAND]: true;
22
61
  readonly kind: 'bearer';
23
- readonly secret: SecretHandle;
62
+ readonly secret: SecretAuthValue;
24
63
  }
25
64
  | {
26
65
  readonly [SECRET_AUTH_BRAND]: true;
27
66
  readonly kind: 'header';
28
67
  readonly header: string;
29
- readonly secret: SecretHandle;
68
+ readonly secret: SecretAuthValue;
30
69
  }
31
70
  );
32
71
 
33
72
  export type SecretAuthInput = SecretAuth | readonly SecretAuth[];
34
73
 
35
- export type SecretAwareRequestInit = PlaySecretAwareRequestInit & {
36
- auth?: SecretAuthInput;
37
- };
74
+ export type SecretAwareRequestInit =
75
+ | PlaySecretAwareRequestInit
76
+ | (Omit<PlaySecretAwareRequestInit, 'auth'> & {
77
+ auth?: SecretAuthInput;
78
+ });
38
79
 
39
80
  function isRecord(value: unknown): value is Record<string | symbol, unknown> {
40
81
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
@@ -44,6 +85,25 @@ export function isSecretHandle(value: unknown): value is SecretHandle {
44
85
  return isRecord(value) && value[SECRET_HANDLE_BRAND] === true;
45
86
  }
46
87
 
88
+ export function isSecretValue(value: unknown): value is SecretValue {
89
+ return (
90
+ isSecretHandle(value) ||
91
+ (isRecord(value) && value[SECRET_VALUE_BRAND] === true)
92
+ );
93
+ }
94
+
95
+ export function isPlaintextSecretPromise(
96
+ value: unknown,
97
+ ): value is PlaintextSecretPromise {
98
+ return (
99
+ Boolean(value) &&
100
+ typeof value === 'object' &&
101
+ (value as Record<string | symbol, unknown>)[
102
+ SECRET_PLAINTEXT_PROMISE_BRAND
103
+ ] === true
104
+ );
105
+ }
106
+
47
107
  export function isSecretAuth(value: unknown): value is SecretAuth {
48
108
  return isRecord(value) && value[SECRET_AUTH_BRAND] === true;
49
109
  }
@@ -69,7 +129,7 @@ export function valueContainsSecret(value: unknown): boolean {
69
129
  const seen = new WeakSet<object>();
70
130
  while (pending.length > 0) {
71
131
  const candidate = pending.pop();
72
- if (isSecretHandle(candidate) || isSecretAuth(candidate)) return true;
132
+ if (isSecretValue(candidate) || isSecretAuth(candidate)) return true;
73
133
  if (typeof candidate === 'string') {
74
134
  if (SECRET_HANDLE_MARKER_RE.test(candidate)) return true;
75
135
  continue;
@@ -99,19 +159,47 @@ export function createSecretHandle(name: string): SecretHandle {
99
159
  } as unknown as SecretHandle;
100
160
  }
101
161
 
102
- export function createBearerSecretAuth(secret: SecretHandle): SecretAuth {
103
- if (!isSecretHandle(secret)) {
104
- throw new Error('ctx.secrets.bearer(...) requires a SecretHandle.');
162
+ export function createPlaintextSecretPromise(
163
+ name: string,
164
+ resolve: () => Promise<string>,
165
+ ): PlaintextSecretPromise {
166
+ const value = resolve() as PlaintextSecretPromise;
167
+ Object.defineProperties(value, {
168
+ [SECRET_PLAINTEXT_PROMISE_BRAND]: { value: true },
169
+ name: { value: name },
170
+ });
171
+ return value;
172
+ }
173
+
174
+ export function createBearerSecretAuth(secret: SecretAuthValue): SecretAuth {
175
+ // A generic promise has no secret provenance or stable receipt marker. Only
176
+ // the promise created by ctx.secrets.get(...) carries the runtime brand.
177
+ if (
178
+ typeof secret !== 'string' &&
179
+ !isSecretValue(secret) &&
180
+ !isPlaintextSecretPromise(secret)
181
+ ) {
182
+ throw new Error(
183
+ 'ctx.secrets.bearer(...) requires a resolved string or legacy secret handle. Await ctx.secrets.get(...) first.',
184
+ );
105
185
  }
106
186
  return { [SECRET_AUTH_BRAND]: true, kind: 'bearer', secret };
107
187
  }
108
188
 
109
189
  export function createHeaderSecretAuth(
110
190
  header: string,
111
- secret: SecretHandle,
191
+ secret: SecretAuthValue,
112
192
  ): SecretAuth {
113
- if (!isSecretHandle(secret)) {
114
- throw new Error('ctx.secrets.header(...) requires a SecretHandle.');
193
+ // Keep this check in sync with bearer: ordinary promises must be awaited
194
+ // before auth construction, while the branded get() promise is compatible.
195
+ if (
196
+ typeof secret !== 'string' &&
197
+ !isSecretValue(secret) &&
198
+ !isPlaintextSecretPromise(secret)
199
+ ) {
200
+ throw new Error(
201
+ 'ctx.secrets.header(...) requires a resolved string or legacy secret handle. Await ctx.secrets.get(...) first.',
202
+ );
115
203
  }
116
204
  if (typeof header !== 'string' || !header.trim()) {
117
205
  throw new Error('ctx.secrets.header(...) requires a header name.');
@@ -124,6 +212,62 @@ export function createHeaderSecretAuth(
124
212
  };
125
213
  }
126
214
 
215
+ export function createSecretConcat(
216
+ parts: readonly (string | SecretValue)[],
217
+ ): SecretValue {
218
+ if (parts.length === 0 || !parts.some(isSecretValue)) {
219
+ throw new Error(
220
+ 'ctx.secrets.concat(...) requires at least one secret value.',
221
+ );
222
+ }
223
+ if (!parts.every((part) => typeof part === 'string' || isSecretValue(part))) {
224
+ throw new Error(
225
+ 'ctx.secrets.concat(...) accepts strings and secret values only.',
226
+ );
227
+ }
228
+ return {
229
+ [SECRET_VALUE_BRAND]: true,
230
+ kind: 'concat',
231
+ parts,
232
+ toString: () => '[secret-expression]',
233
+ toJSON: () => {
234
+ throw new Error(
235
+ 'Secret expressions cannot be serialized. Use them only with ctx.secrets auth helpers.',
236
+ );
237
+ },
238
+ } as SecretValue;
239
+ }
240
+
241
+ export function createBase64SecretValue(value: SecretValue): SecretValue {
242
+ if (!isSecretValue(value)) {
243
+ throw new Error('ctx.secrets.base64(...) requires a secret value.');
244
+ }
245
+ return {
246
+ [SECRET_VALUE_BRAND]: true,
247
+ kind: 'base64',
248
+ value,
249
+ toString: () => '[secret-expression]',
250
+ toJSON: () => {
251
+ throw new Error(
252
+ 'Secret expressions cannot be serialized. Use them only with ctx.secrets auth helpers.',
253
+ );
254
+ },
255
+ } as SecretValue;
256
+ }
257
+
258
+ export function secretValueMarker(value: SecretValue): string {
259
+ if (isSecretHandle(value)) return `[secret:${value.name}]`;
260
+ if (value.kind === 'base64')
261
+ return `[secret-base64:${secretValueMarker(value.value)}]`;
262
+ return `[secret-concat:${value.parts
263
+ .map((part) =>
264
+ typeof part === 'string'
265
+ ? `[literal:${sha256Hex(part)}]`
266
+ : secretValueMarker(part),
267
+ )
268
+ .join('+')}]`;
269
+ }
270
+
127
271
  export function secretAuthHeaderMarkers(
128
272
  auth: SecretAuthInput | undefined,
129
273
  ): Record<string, string> {
@@ -136,7 +280,12 @@ export function secretAuthHeaderMarkers(
136
280
  `ctx.fetch cannot attach more than one secret to the ${header} header.`,
137
281
  );
138
282
  }
139
- markers[header] = `[secret:${entry.secret.name}]`;
283
+ markers[header] =
284
+ typeof entry.secret === 'string'
285
+ ? `[secret-plaintext:${sha256Hex(entry.secret)}]`
286
+ : isPlaintextSecretPromise(entry.secret)
287
+ ? `[secret:${entry.secret.name}]`
288
+ : secretValueMarker(entry.secret as SecretValue);
140
289
  }
141
290
  return markers;
142
291
  }