deepline 0.1.66 → 0.1.69

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.
@@ -128,6 +128,19 @@ import {
128
128
  } from '../../../shared_libs/play-runtime/csv-rename';
129
129
  import { coordinatorRequestHeaders } from '../../../shared_libs/play-runtime/coordinator-headers';
130
130
  import { normalizePlayRunFailure } from '../../../shared_libs/play-runtime/run-failure';
131
+ import { createSecretRedactionContext } from '../../../shared_libs/play-runtime/secret-redaction';
132
+ import {
133
+ assertNoSecretTaint,
134
+ createBearerSecretAuth,
135
+ createHeaderSecretAuth,
136
+ createSecretHandle,
137
+ isSecretAuth,
138
+ secretAuthHeaderMarkers,
139
+ valueContainsSecret,
140
+ type SecretAuth,
141
+ type SecretAwareRequestInit,
142
+ type SecretHandle,
143
+ } from '../../../shared_libs/play-runtime/secret-capability';
131
144
  import type {
132
145
  LiveNodeProgressMap,
133
146
  LiveNodeProgressSnapshot,
@@ -3183,6 +3196,39 @@ function createMinimalWorkerCtx(
3183
3196
  const inFlightChildCallsByPlayName: Record<string, number> = {};
3184
3197
  let inFlightChildPlayCalls = 0;
3185
3198
  const childPlaySlotWaiters: Array<() => void> = [];
3199
+ const secretRedactor = createSecretRedactionContext();
3200
+
3201
+ const resolveSecretAuth = async (auth?: SecretAuth) => {
3202
+ if (!auth) return {};
3203
+ const response = await fetchRuntimeApi(
3204
+ req.baseUrl,
3205
+ '/api/v2/plays/internal/runtime',
3206
+ {
3207
+ method: 'POST',
3208
+ headers: {
3209
+ authorization: `Bearer ${req.executorToken}`,
3210
+ 'content-type': 'application/json',
3211
+ },
3212
+ body: JSON.stringify({
3213
+ action: 'resolve_secret',
3214
+ name: auth.secret.name,
3215
+ playName: req.playName,
3216
+ }),
3217
+ },
3218
+ );
3219
+ if (!response.ok) {
3220
+ throw new Error(`Secret ${auth.secret.name} is not available to this run.`);
3221
+ }
3222
+ const payload = (await response.json()) as { value?: unknown };
3223
+ const value = typeof payload.value === 'string' ? payload.value : null;
3224
+ if (!value) {
3225
+ throw new Error(`Secret ${auth.secret.name} is not available to this run.`);
3226
+ }
3227
+ secretRedactor.register(value);
3228
+ return auth.kind === 'bearer'
3229
+ ? { authorization: `Bearer ${value}` }
3230
+ : { [auth.header.toLowerCase()]: value };
3231
+ };
3186
3232
 
3187
3233
  const acquireChildPlaySlot = async (): Promise<() => void> => {
3188
3234
  while (
@@ -4007,6 +4053,7 @@ function createMinimalWorkerCtx(
4007
4053
  */
4008
4054
  signal: abortSignal ?? new AbortController().signal,
4009
4055
  log(message: string) {
4056
+ assertNoSecretTaint(message, 'ctx.log');
4010
4057
  emitEvent({
4011
4058
  type: 'log',
4012
4059
  level: 'info',
@@ -4184,10 +4231,11 @@ function createMinimalWorkerCtx(
4184
4231
  'ctx.map(key, rows, fields, options) was removed. Use ctx.map(key, rows).step(...).run(options).',
4185
4232
  );
4186
4233
  },
4187
- tools: {
4188
- async execute(requestArg: unknown): Promise<unknown> {
4234
+ tools: {
4235
+ async execute(requestArg: unknown): Promise<unknown> {
4189
4236
  assertNotAborted(abortSignal);
4190
4237
  const request = normalizeToolExecuteArgs(requestArg);
4238
+ assertNoSecretTaint(request.input, 'ctx.tools.execute input');
4191
4239
  return await executeWithRuntimeReceipt(
4192
4240
  `tool:${request.id}:${deriveToolRequestIdentity({
4193
4241
  toolId: request.toolId,
@@ -4257,9 +4305,10 @@ function createMinimalWorkerCtx(
4257
4305
  timeoutMs?: number;
4258
4306
  staleAfterSeconds?: number;
4259
4307
  },
4260
- ): Promise<unknown> {
4261
- const normalizedKey = normalizeContextKey(key, 'runPlay');
4262
- const resolvedName = resolvePlayRefName(playRef);
4308
+ ): Promise<unknown> {
4309
+ const normalizedKey = normalizeContextKey(key, 'runPlay');
4310
+ const resolvedName = resolvePlayRefName(playRef);
4311
+ assertNoSecretTaint(input, 'ctx.runPlay input');
4263
4312
  if (!resolvedName) {
4264
4313
  throw new Error('ctx.runPlay(...) requires a resolvable play name.');
4265
4314
  }
@@ -4498,17 +4547,37 @@ function createMinimalWorkerCtx(
4498
4547
  }
4499
4548
  });
4500
4549
  },
4501
- async fetch(
4502
- key: string,
4503
- input: string | URL,
4504
- init: RequestInit = {},
4505
- options?: { staleAfterSeconds?: number },
4506
- ): Promise<WorkerFetchResponse> {
4507
- assertNotAborted(abortSignal);
4508
- const normalizedKey = normalizeContextKey(key, 'fetch');
4509
- const url = input.toString();
4510
- const method = (init.method ?? 'GET').toUpperCase();
4511
- const safeHeaders = normalizeFetchHeaders(init.headers);
4550
+ async fetch(
4551
+ key: string,
4552
+ input: string | URL,
4553
+ init: SecretAwareRequestInit = {},
4554
+ options?: { staleAfterSeconds?: number },
4555
+ ): Promise<WorkerFetchResponse> {
4556
+ assertNotAborted(abortSignal);
4557
+ const normalizedKey = normalizeContextKey(key, 'fetch');
4558
+ if (
4559
+ valueContainsSecret(input) ||
4560
+ valueContainsSecret(init.body)
4561
+ ) {
4562
+ throw new Error(
4563
+ 'ctx.fetch does not allow secrets in the URL or body. Use an approved secret auth helper.',
4564
+ );
4565
+ }
4566
+ if (valueContainsSecret(init.headers)) {
4567
+ throw new Error(
4568
+ 'ctx.fetch does not allow raw secret headers. Use ctx.secrets.bearer(...) or ctx.secrets.header(...).',
4569
+ );
4570
+ }
4571
+ if (init.auth !== undefined && !isSecretAuth(init.auth)) {
4572
+ throw new Error('ctx.fetch auth must come from ctx.secrets.');
4573
+ }
4574
+ const url = input.toString();
4575
+ const method = (init.method ?? 'GET').toUpperCase();
4576
+ const secretHeaderMarkers = secretAuthHeaderMarkers(init.auth);
4577
+ const safeHeaders = {
4578
+ ...normalizeFetchHeaders(init.headers),
4579
+ ...secretHeaderMarkers,
4580
+ };
4512
4581
  const body = fetchBodyIdentity(init.body);
4513
4582
  const hasIdempotencyKey =
4514
4583
  safeHeaders['idempotency-key'] !== undefined ||
@@ -4524,20 +4593,44 @@ function createMinimalWorkerCtx(
4524
4593
  safeHeaders,
4525
4594
  url,
4526
4595
  })}${staleRuntimeSuffix(options?.staleAfterSeconds)}`;
4527
- return await executeWithRuntimeReceipt(receiptKey, async () => {
4528
- const response = await fetch(url, init);
4529
- assertNotAborted(abortSignal);
4530
- const bodyText = await response.text();
4531
- return {
4532
- ok: response.ok,
4533
- status: response.status,
4534
- statusText: response.statusText,
4535
- url: response.url,
4536
- headers: Object.fromEntries(response.headers.entries()),
4537
- bodyText,
4538
- json: parseFetchJsonOrNull(bodyText),
4596
+ return await executeWithRuntimeReceipt(receiptKey, async () => {
4597
+ const secretHeaders = await resolveSecretAuth(init.auth);
4598
+ const headers = {
4599
+ ...normalizeFetchHeaders(init.headers),
4600
+ ...secretHeaders,
4539
4601
  };
4540
- });
4602
+ const fetchInit = { ...init, headers };
4603
+ delete fetchInit.auth;
4604
+ const response = await fetch(url, fetchInit);
4605
+ assertNotAborted(abortSignal);
4606
+ const bodyText = await response.text();
4607
+ const redactedBodyText = secretRedactor.redactString(bodyText);
4608
+ return {
4609
+ ok: response.ok,
4610
+ status: response.status,
4611
+ statusText: response.statusText,
4612
+ url: response.url,
4613
+ headers: secretRedactor.redact(
4614
+ Object.fromEntries(response.headers.entries()),
4615
+ ) as Record<string, string>,
4616
+ bodyText: redactedBodyText,
4617
+ json: secretRedactor.redact(parseFetchJsonOrNull(bodyText)),
4618
+ };
4619
+ });
4620
+ },
4621
+ secrets: {
4622
+ get(name: string): SecretHandle {
4623
+ if (typeof name !== 'string' || !name.trim()) {
4624
+ throw new Error('ctx.secrets.get(name) requires a non-empty name.');
4625
+ }
4626
+ return createSecretHandle(name.trim());
4627
+ },
4628
+ bearer(secret: SecretHandle): SecretAuth {
4629
+ return createBearerSecretAuth(secret);
4630
+ },
4631
+ header(header: string, secret: SecretHandle): SecretAuth {
4632
+ return createHeaderSecretAuth(header, secret);
4633
+ },
4541
4634
  },
4542
4635
  async waitForEvent(
4543
4636
  eventType: string,
@@ -57,6 +57,9 @@ import type {
57
57
  PublishPlayVersionResult,
58
58
  StartPlayRunRequest,
59
59
  DeletePlayResult,
60
+ SharePageStatus,
61
+ PublishSharePageRequest,
62
+ UpdateSharePageRequest,
60
63
  ToolDefinition,
61
64
  ToolSearchOptions,
62
65
  ToolSearchResult,
@@ -158,6 +161,19 @@ export type PlaySheetRowsResult = {
158
161
  deltaCursor?: number;
159
162
  };
160
163
 
164
+ export type PlaySecretMetadata = {
165
+ _id: string;
166
+ orgId: string;
167
+ scope: 'org' | 'play';
168
+ playName?: string;
169
+ name: string;
170
+ status: string;
171
+ hasValue: boolean;
172
+ createdAt: number;
173
+ updatedAt: number;
174
+ lastUsedAt?: number;
175
+ };
176
+
161
177
  export type RunsNamespace = {
162
178
  get: (runId: string, options?: { full?: boolean }) => Promise<PlayStatus>;
163
179
  list: (options: RunsListOptions) => Promise<PlayRunListItem[]>;
@@ -571,6 +587,30 @@ export class DeeplineClient {
571
587
  };
572
588
  }
573
589
 
590
+ // ——————————————————————————————————————————————————————————
591
+ // Secrets
592
+ // ——————————————————————————————————————————————————————————
593
+
594
+ async listSecrets(): Promise<PlaySecretMetadata[]> {
595
+ const response = await this.http.get<{ secrets?: PlaySecretMetadata[] }>(
596
+ '/api/v2/secrets',
597
+ );
598
+ return Array.isArray(response.secrets) ? response.secrets : [];
599
+ }
600
+
601
+ async checkSecret(name: string): Promise<PlaySecretMetadata | null> {
602
+ const normalized = name.trim().toUpperCase();
603
+ const secrets = await this.listSecrets();
604
+ return (
605
+ secrets.find(
606
+ (secret) =>
607
+ secret.name === normalized &&
608
+ secret.status === 'active' &&
609
+ secret.hasValue,
610
+ ) ?? null
611
+ );
612
+ }
613
+
574
614
  // ——————————————————————————————————————————————————————————
575
615
  // Tools
576
616
  // ——————————————————————————————————————————————————————————
@@ -1625,6 +1665,76 @@ export class DeeplineClient {
1625
1665
  return this.http.delete<DeletePlayResult>(`/api/v2/plays/${encodedName}`);
1626
1666
  }
1627
1667
 
1668
+ // ——————————————————————————————————————————————————————————
1669
+ // Plays — public share pages
1670
+ // ——————————————————————————————————————————————————————————
1671
+
1672
+ /**
1673
+ * Current share status for a play: the public page (if any), the published
1674
+ * copy, and the revision picker. Read-only.
1675
+ */
1676
+ async getSharePage(name: string): Promise<SharePageStatus> {
1677
+ const encodedName = encodeURIComponent(name);
1678
+ return this.http.get<SharePageStatus>(`/api/v2/plays/${encodedName}/share`);
1679
+ }
1680
+
1681
+ /**
1682
+ * Publish (or repoint) the play's public share page to a revision. Requires
1683
+ * `acknowledgedUnlisted: true` — the page is publicly viewable. Org-admin only.
1684
+ */
1685
+ async publishSharePage(
1686
+ name: string,
1687
+ request: PublishSharePageRequest,
1688
+ ): Promise<SharePageStatus> {
1689
+ const encodedName = encodeURIComponent(name);
1690
+ return this.http.post<SharePageStatus>(
1691
+ `/api/v2/plays/${encodedName}/share`,
1692
+ request,
1693
+ );
1694
+ }
1695
+
1696
+ /**
1697
+ * Update share-page settings (SEO indexing, credit-cost / latency display)
1698
+ * without moving the published pointer. Org-admin only.
1699
+ */
1700
+ async updateSharePage(
1701
+ name: string,
1702
+ request: UpdateSharePageRequest,
1703
+ ): Promise<SharePageStatus> {
1704
+ const encodedName = encodeURIComponent(name);
1705
+ return this.http.patch<SharePageStatus>(
1706
+ `/api/v2/plays/${encodedName}/share`,
1707
+ request,
1708
+ );
1709
+ }
1710
+
1711
+ /**
1712
+ * Unshare: hard-delete the play's public page and its cards. Returns the
1713
+ * fresh status (now `share: null`). Org-admin only. Idempotent — a no-op when
1714
+ * the play was never published.
1715
+ */
1716
+ async unpublishSharePage(name: string): Promise<SharePageStatus> {
1717
+ const encodedName = encodeURIComponent(name);
1718
+ return this.http.delete<SharePageStatus>(
1719
+ `/api/v2/plays/${encodedName}/share`,
1720
+ );
1721
+ }
1722
+
1723
+ /**
1724
+ * Regenerate the LLM landing-page copy for a revision (defaults to the
1725
+ * published one). Org-admin only.
1726
+ */
1727
+ async regenerateSharePage(
1728
+ name: string,
1729
+ request: { revisionId?: string } = {},
1730
+ ): Promise<SharePageStatus> {
1731
+ const encodedName = encodeURIComponent(name);
1732
+ return this.http.post<SharePageStatus>(
1733
+ `/api/v2/plays/${encodedName}/share/regenerate`,
1734
+ request,
1735
+ );
1736
+ }
1737
+
1628
1738
  // ——————————————————————————————————————————————————————————
1629
1739
  // Plays — high-level orchestration
1630
1740
  // ——————————————————————————————————————————————————————————
@@ -35,7 +35,7 @@ import {
35
35
  const MAX_DIAGNOSTIC_HEADER_LENGTH = 120;
36
36
 
37
37
  interface RequestOptions {
38
- method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
38
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
39
39
  body?: unknown;
40
40
  formData?: FormData | (() => FormData);
41
41
  headers?: Record<string, string>;
@@ -392,6 +392,14 @@ export class HttpClient {
392
392
  });
393
393
  }
394
394
 
395
+ async patch<T = unknown>(
396
+ path: string,
397
+ body: unknown,
398
+ headers?: Record<string, string>,
399
+ ): Promise<T> {
400
+ return this.request<T>(path, { method: 'PATCH', body, headers });
401
+ }
402
+
395
403
  /**
396
404
  * Send a DELETE request.
397
405
  *
@@ -146,6 +146,33 @@ export type PlayBindings = {
146
146
  /** IANA timezone (e.g. `'America/New_York'`). Defaults to UTC. */
147
147
  timezone?: string;
148
148
  };
149
+ /**
150
+ * Customer-authored play secrets this play is allowed to use at runtime.
151
+ * Values are never bundled or exposed by the SDK; access them with
152
+ * `ctx.secrets.get("NAME")` and approved helpers such as
153
+ * `ctx.secrets.bearer(handle)`.
154
+ */
155
+ secrets?: readonly string[];
156
+ };
157
+
158
+ declare const SECRET_HANDLE_BRAND: unique symbol;
159
+
160
+ export type SecretHandle = {
161
+ readonly [SECRET_HANDLE_BRAND]: never;
162
+ readonly name: string;
163
+ toString(): string;
164
+ toJSON(): never;
165
+ };
166
+
167
+ export type SecretAuth = {
168
+ readonly kind: 'bearer' | 'header';
169
+ readonly secret: SecretHandle;
170
+ readonly header?: string;
171
+ };
172
+
173
+ export type SecretAwareRequestInit = Omit<RequestInit, 'headers'> & {
174
+ headers?: HeadersInit;
175
+ auth?: SecretAuth;
149
176
  };
150
177
 
151
178
  export type LoosePlayObject = {
@@ -523,7 +550,7 @@ export interface DeeplinePlayRuntimeContext {
523
550
  fetch(
524
551
  key: string,
525
552
  url: string | URL,
526
- init?: RequestInit,
553
+ init?: SecretAwareRequestInit,
527
554
  options?: { staleAfterSeconds?: number },
528
555
  ): Promise<{
529
556
  ok: boolean;
@@ -534,6 +561,11 @@ export interface DeeplinePlayRuntimeContext {
534
561
  bodyText: string;
535
562
  json: unknown | null;
536
563
  }>;
564
+ secrets: {
565
+ get(name: string): SecretHandle;
566
+ bearer(secret: SecretHandle): SecretAuth;
567
+ header(header: string, secret: SecretHandle): SecretAuth;
568
+ };
537
569
  runPlay<TOutput = unknown>(
538
570
  key: string,
539
571
  playRef: string | PlayReferenceLike,
@@ -17,6 +17,7 @@ import {
17
17
  type PlayArtifactKind,
18
18
  } from '../../../shared_libs/play-runtime/backend.js';
19
19
  import { resolveExecutionProfile } from '../../../shared_libs/play-runtime/profiles.js';
20
+ import { validatePlaySourceFilesHaveNoInlineSecrets } from '../../../shared_libs/plays/secret-guardrails.js';
20
21
  import { discoverPackagedLocalFiles } from './local-file-discovery.js';
21
22
 
22
23
  export type {
@@ -149,11 +150,13 @@ export async function bundlePlayFile(
149
150
  filePath: string,
150
151
  options: BundlePlayFileOptions = {},
151
152
  ): Promise<BundledPlayFileResult> {
152
- return bundlePlayFileCore(filePath, {
153
+ const result = await bundlePlayFileCore(filePath, {
153
154
  target: options.target ?? defaultPlayBundleTarget(),
154
155
  exportName: options.exportName,
155
156
  adapter: createSdkPlayBundlingAdapter(),
156
157
  });
158
+ if (result.success) validatePlaySourceFilesHaveNoInlineSecrets(result.sourceFiles);
159
+ return result;
157
160
  }
158
161
 
159
162
  export { PLAY_ARTIFACT_KINDS };
@@ -50,10 +50,10 @@ export type SdkRelease = {
50
50
  };
51
51
 
52
52
  export const SDK_RELEASE = {
53
- version: '0.1.66',
53
+ version: '0.1.69',
54
54
  apiContract: '2026-05-play-bootstrap-dataset-summary',
55
55
  supportPolicy: {
56
- latest: '0.1.66',
56
+ latest: '0.1.69',
57
57
  minimumSupported: '0.1.53',
58
58
  deprecatedBelow: '0.1.53',
59
59
  },
@@ -889,3 +889,62 @@ export interface DeletePlayResult {
889
889
  deletedBindingCount: number;
890
890
  deletedRunCount: number;
891
891
  }
892
+
893
+ // ——————————————————————————————————————————————————————————
894
+ // Shareable play pages
895
+ // ——————————————————————————————————————————————————————————
896
+
897
+ /** Owner-facing view of a play's public share page. */
898
+ export interface SharePageOwnerView {
899
+ shareSlug: string;
900
+ publishedRevisionId: string;
901
+ publishedVersion: number;
902
+ visibility: string;
903
+ seoIndexing: 'index' | 'noindex';
904
+ showAverageDeeplineCost: boolean;
905
+ showAverageLatency: boolean;
906
+ /** Stable public path, e.g. `/p/{shareSlug}`. */
907
+ publicPath: string;
908
+ /** Version-pinned canonical path, e.g. `/p/{shareSlug}/v/{version}`. */
909
+ canonicalPath: string;
910
+ createdAt: number;
911
+ updatedAt: number;
912
+ }
913
+
914
+ /** One row in the owner-facing revision picker for sharing. */
915
+ export interface SharePageRevisionOption {
916
+ revisionId: string;
917
+ version: number;
918
+ isLive: boolean;
919
+ isWorking: boolean;
920
+ isPublished: boolean;
921
+ hasMap: boolean;
922
+ hasCard: boolean;
923
+ createdAt: number;
924
+ }
925
+
926
+ /** Status payload from `GET/POST/PATCH /api/v2/plays/:name/share`. */
927
+ export interface SharePageStatus {
928
+ playName: string;
929
+ share: SharePageOwnerView | null;
930
+ publishedCopy: unknown | null;
931
+ revisions: SharePageRevisionOption[];
932
+ /** Present on publish responses when share-card generation was non-strict. */
933
+ warning?: string | null;
934
+ }
935
+
936
+ export interface PublishSharePageRequest {
937
+ /** The revision to publish/repoint the public page to. */
938
+ revisionId: string;
939
+ /** Must be true — acknowledges the page is publicly viewable. */
940
+ acknowledgedUnlisted: true;
941
+ showAverageDeeplineCost?: boolean;
942
+ showAverageLatency?: boolean;
943
+ seoIndexing?: 'index' | 'noindex';
944
+ }
945
+
946
+ export interface UpdateSharePageRequest {
947
+ showAverageDeeplineCost?: boolean;
948
+ showAverageLatency?: boolean;
949
+ seoIndexing?: 'index' | 'noindex';
950
+ }
@@ -0,0 +1,103 @@
1
+ const SECRET_HANDLE_BRAND = Symbol.for('deepline.secret.handle');
2
+ const SECRET_AUTH_BRAND = Symbol.for('deepline.secret.auth');
3
+ const SECRET_HANDLE_MARKER_RE = /\[secret:[A-Z0-9_ -]+\]/i;
4
+
5
+ export type SecretHandle = {
6
+ readonly [SECRET_HANDLE_BRAND]: true;
7
+ readonly name: string;
8
+ toString(): string;
9
+ toJSON(): never;
10
+ };
11
+
12
+ export type SecretAuth =
13
+ | {
14
+ readonly [SECRET_AUTH_BRAND]: true;
15
+ readonly kind: 'bearer';
16
+ readonly secret: SecretHandle;
17
+ }
18
+ | {
19
+ readonly [SECRET_AUTH_BRAND]: true;
20
+ readonly kind: 'header';
21
+ readonly header: string;
22
+ readonly secret: SecretHandle;
23
+ };
24
+
25
+ export type SecretAwareRequestInit = RequestInit & {
26
+ auth?: SecretAuth;
27
+ };
28
+
29
+ function isRecord(value: unknown): value is Record<string | symbol, unknown> {
30
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
31
+ }
32
+
33
+ export function isSecretHandle(value: unknown): value is SecretHandle {
34
+ return isRecord(value) && value[SECRET_HANDLE_BRAND] === true;
35
+ }
36
+
37
+ export function isSecretAuth(value: unknown): value is SecretAuth {
38
+ return isRecord(value) && value[SECRET_AUTH_BRAND] === true;
39
+ }
40
+
41
+ export function valueContainsSecret(value: unknown): boolean {
42
+ if (isSecretHandle(value) || isSecretAuth(value)) return true;
43
+ if (typeof value === 'string') return SECRET_HANDLE_MARKER_RE.test(value);
44
+ if (Array.isArray(value)) return value.some(valueContainsSecret);
45
+ if (isRecord(value)) return Object.values(value).some(valueContainsSecret);
46
+ return false;
47
+ }
48
+
49
+ export function createSecretHandle(name: string): SecretHandle {
50
+ return {
51
+ [SECRET_HANDLE_BRAND]: true,
52
+ name,
53
+ toString: () => `[secret:${name}]`,
54
+ toJSON: () => {
55
+ throw new Error(
56
+ `Secret ${name} cannot be serialized. Use an approved ctx.secrets helper.`,
57
+ );
58
+ },
59
+ };
60
+ }
61
+
62
+ export function createBearerSecretAuth(secret: SecretHandle): SecretAuth {
63
+ if (!isSecretHandle(secret)) {
64
+ throw new Error('ctx.secrets.bearer(...) requires a SecretHandle.');
65
+ }
66
+ return { [SECRET_AUTH_BRAND]: true, kind: 'bearer', secret };
67
+ }
68
+
69
+ export function createHeaderSecretAuth(
70
+ header: string,
71
+ secret: SecretHandle,
72
+ ): SecretAuth {
73
+ if (!isSecretHandle(secret)) {
74
+ throw new Error('ctx.secrets.header(...) requires a SecretHandle.');
75
+ }
76
+ if (typeof header !== 'string' || !header.trim()) {
77
+ throw new Error('ctx.secrets.header(...) requires a header name.');
78
+ }
79
+ return {
80
+ [SECRET_AUTH_BRAND]: true,
81
+ kind: 'header',
82
+ header: header.trim(),
83
+ secret,
84
+ };
85
+ }
86
+
87
+ export function secretAuthHeaderMarkers(
88
+ auth: SecretAuth | undefined,
89
+ ): Record<string, string> {
90
+ if (!auth) return {};
91
+ if (auth.kind === 'bearer') {
92
+ return { authorization: `[secret:${auth.secret.name}]` };
93
+ }
94
+ return { [auth.header.toLowerCase()]: `[secret:${auth.secret.name}]` };
95
+ }
96
+
97
+ export function assertNoSecretTaint(value: unknown, sink: string): void {
98
+ if (valueContainsSecret(value)) {
99
+ throw new Error(
100
+ `${sink} cannot receive secret handles or secret-tainted values. Use an approved ctx.secrets helper.`,
101
+ );
102
+ }
103
+ }