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.
@@ -5,6 +5,139 @@ const PRIVATE_KEY_PATTERN =
5
5
  const BEARER_LITERAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i;
6
6
  const ASSIGNMENT_SECRET_LITERAL_PATTERN =
7
7
  /\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*['"][^'"]{12,}['"]/i;
8
+ const SECRET_AUTH_LITERAL_FINDING =
9
+ 'literal credential passed to ctx.secrets auth helper';
10
+
11
+ function skipQuoted(source: string, start: number): number {
12
+ const quote = source[start];
13
+ let index = start + 1;
14
+ while (index < source.length) {
15
+ if (source[index] === '\\') {
16
+ index += 2;
17
+ } else if (source[index] === quote) {
18
+ return index + 1;
19
+ } else {
20
+ index += 1;
21
+ }
22
+ }
23
+ return source.length;
24
+ }
25
+
26
+ function skipTrivia(source: string, start: number): number {
27
+ let index = start;
28
+ while (index < source.length) {
29
+ if (/\s/.test(source[index])) {
30
+ index += 1;
31
+ } else if (source.startsWith('//', index)) {
32
+ index = source.indexOf('\n', index + 2);
33
+ if (index === -1) return source.length;
34
+ } else if (source.startsWith('/*', index)) {
35
+ index = source.indexOf('*/', index + 2);
36
+ if (index === -1) return source.length;
37
+ index += 2;
38
+ } else {
39
+ return index;
40
+ }
41
+ }
42
+ return index;
43
+ }
44
+
45
+ function consumeIdentifier(
46
+ source: string,
47
+ start: number,
48
+ expected: string,
49
+ ): number | undefined {
50
+ return source.startsWith(expected, start) &&
51
+ !/[A-Za-z0-9_$]/.test(source[start - 1] ?? '') &&
52
+ !/[A-Za-z0-9_$]/.test(source[start + expected.length] ?? '')
53
+ ? start + expected.length
54
+ : undefined;
55
+ }
56
+
57
+ function consumeMember(
58
+ source: string,
59
+ start: number,
60
+ expected: string,
61
+ ): number | undefined {
62
+ let index = skipTrivia(source, start);
63
+ if (source[index] === '.') {
64
+ return consumeIdentifier(source, skipTrivia(source, index + 1), expected);
65
+ }
66
+ if (source[index] !== '[') return undefined;
67
+ index = skipTrivia(source, index + 1);
68
+ if (source[index] !== "'" && source[index] !== '"') return undefined;
69
+ const end = skipQuoted(source, index);
70
+ if (source.slice(index + 1, end - 1) !== expected) return undefined;
71
+ index = skipTrivia(source, end);
72
+ return source[index] === ']' ? index + 1 : undefined;
73
+ }
74
+
75
+ function skipExpression(source: string, start: number): number {
76
+ let index = start;
77
+ let depth = 0;
78
+ while (index < source.length) {
79
+ const char = source[index];
80
+ if (char === "'" || char === '"' || char === '`') {
81
+ index = skipQuoted(source, index);
82
+ continue;
83
+ }
84
+ if (source.startsWith('//', index) || source.startsWith('/*', index)) {
85
+ index = skipTrivia(source, index);
86
+ continue;
87
+ }
88
+ if (char === '(' || char === '[' || char === '{') {
89
+ depth += 1;
90
+ } else if (char === ')' || char === ']' || char === '}') {
91
+ if (depth === 0) return index;
92
+ depth -= 1;
93
+ } else if (char === ',' && depth === 0) {
94
+ return index;
95
+ }
96
+ index += 1;
97
+ }
98
+ return index;
99
+ }
100
+
101
+ function isDirectStringLiteral(source: string, start: number): boolean {
102
+ const quote = source[start];
103
+ if (quote !== "'" && quote !== '"' && quote !== '`') return false;
104
+ const end = skipQuoted(source, start);
105
+ return quote !== '`' || !source.slice(start, end).includes('${');
106
+ }
107
+
108
+ function hasLiteralSecretAuthCredential(source: string): boolean {
109
+ for (let index = 0; index < source.length; index += 1) {
110
+ const char = source[index];
111
+ if (char === "'" || char === '"' || char === '`') {
112
+ index = skipQuoted(source, index) - 1;
113
+ continue;
114
+ }
115
+ if (source.startsWith('//', index) || source.startsWith('/*', index)) {
116
+ index = skipTrivia(source, index) - 1;
117
+ continue;
118
+ }
119
+
120
+ const afterCtx = consumeIdentifier(source, index, 'ctx');
121
+ if (!afterCtx) continue;
122
+ const afterSecrets = consumeMember(source, afterCtx, 'secrets');
123
+ if (!afterSecrets) continue;
124
+ const afterHeader = consumeMember(source, afterSecrets, 'header');
125
+ const helper = afterHeader ? 'header' : 'bearer';
126
+ const afterHelper =
127
+ afterHeader ?? consumeMember(source, afterSecrets, 'bearer');
128
+ if (!afterHelper) continue;
129
+ const afterOpen = skipTrivia(source, afterHelper);
130
+ if (source[afterOpen] !== '(') continue;
131
+
132
+ const firstArgument = skipTrivia(source, afterOpen + 1);
133
+ const credential =
134
+ helper === 'header'
135
+ ? skipTrivia(source, skipExpression(source, firstArgument) + 1)
136
+ : firstArgument;
137
+ if (isDirectStringLiteral(source, credential)) return true;
138
+ }
139
+ return false;
140
+ }
8
141
  /**
9
142
  * Returns the inline-secret findings in a string (empty if none). The throwing
10
143
  * validator below and the workflows→plays migration validator both call this so
@@ -22,6 +155,9 @@ export function collectInlineSecretFindings(sourceCode: string): string[] {
22
155
  if (ASSIGNMENT_SECRET_LITERAL_PATTERN.test(sourceCode)) {
23
156
  findings.push('secret-looking assignment literal');
24
157
  }
158
+ if (hasLiteralSecretAuthCredential(sourceCode)) {
159
+ findings.push(SECRET_AUTH_LITERAL_FINDING);
160
+ }
25
161
  return [...new Set(findings)];
26
162
  }
27
163
 
@@ -36,7 +172,7 @@ export function validatePlaySourceHasNoInlineSecrets(input: {
36
172
  `Play source ${input.filePath} appears to contain inline secret material: ${[
37
173
  ...new Set(findings),
38
174
  ].join(', ')}.`,
39
- 'Author secrets in the dashboard and use ctx.secrets.get("NAME") with an approved helper such as ctx.secrets.bearer(handle).',
175
+ 'Author secrets in the dashboard, declare them in the Play\'s top-level secrets option, and read them at runtime with await ctx.secrets.get("NAME").',
40
176
  ].join(' '),
41
177
  );
42
178
  }
@@ -13,6 +13,8 @@ export type SafeFetchOptions = {
13
13
  fetchImpl?: SafeFetchImplementation;
14
14
  maxRedirects?: number;
15
15
  sensitiveHeaders?: Iterable<string>;
16
+ /** Drop every caller-supplied header when a redirect changes origin. */
17
+ stripHeadersOnCrossOriginRedirect?: boolean;
16
18
  };
17
19
 
18
20
  function removeHeader(headers: Headers, name: string) {
@@ -29,10 +31,11 @@ function requestInitForRedirect(
29
31
  to: URL,
30
32
  status: number,
31
33
  sensitiveHeaders: Iterable<string>,
34
+ stripHeadersOnCrossOriginRedirect: boolean,
32
35
  ): RequestInit {
33
- const headers = new Headers(init.headers);
36
+ let headers = new Headers(init.headers);
34
37
  let method = String(init.method ?? 'GET').toUpperCase();
35
- let body = init.body;
38
+ let body = init.body ?? undefined;
36
39
 
37
40
  if (
38
41
  status === 303 ||
@@ -45,11 +48,23 @@ function requestInitForRedirect(
45
48
  }
46
49
 
47
50
  if (from.origin !== to.origin) {
48
- removeHeader(headers, 'authorization');
49
- removeHeader(headers, 'cookie');
50
- removeHeader(headers, 'proxy-authorization');
51
- for (const header of sensitiveHeaders) {
52
- removeHeader(headers, header);
51
+ if (body !== undefined) {
52
+ throw new Error(
53
+ 'Cross-origin redirect blocked because it would replay a request body.',
54
+ );
55
+ }
56
+ if (stripHeadersOnCrossOriginRedirect) {
57
+ // Callers that can construct credentials from plaintext secrets cannot
58
+ // reliably identify every transformed value. Do not forward any caller
59
+ // header across an origin boundary in that trust model.
60
+ headers = new Headers();
61
+ } else {
62
+ removeHeader(headers, 'authorization');
63
+ removeHeader(headers, 'cookie');
64
+ removeHeader(headers, 'proxy-authorization');
65
+ for (const header of sensitiveHeaders) {
66
+ removeHeader(headers, header);
67
+ }
53
68
  }
54
69
  }
55
70
 
@@ -71,6 +86,8 @@ export async function safePublicFetch(
71
86
  const maxRedirects = options.maxRedirects ?? 10;
72
87
  const fetchImpl = options.fetchImpl ?? fetch;
73
88
  const sensitiveHeaders = options.sensitiveHeaders ?? [];
89
+ const stripHeadersOnCrossOriginRedirect =
90
+ options.stripHeadersOnCrossOriginRedirect === true;
74
91
  let currentUrl = assertPublicHttpUrl(input);
75
92
  let currentInit: RequestInit = {
76
93
  ...init,
@@ -117,6 +134,7 @@ export async function safePublicFetch(
117
134
  nextUrl,
118
135
  response.status,
119
136
  sensitiveHeaders,
137
+ stripHeadersOnCrossOriginRedirect,
120
138
  );
121
139
  currentUrl = nextUrl;
122
140
  }
package/dist/cli/index.js CHANGED
@@ -1047,7 +1047,7 @@ var SDK_RELEASE = {
1047
1047
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
1048
1048
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
1049
1049
  // getters keep their established compatibility behavior.
1050
- version: "0.3.7",
1050
+ version: "0.3.8",
1051
1051
  updateSummary: "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.",
1052
1052
  contracts: {
1053
1053
  api: {
@@ -2020,10 +2020,30 @@ function redactSecretLikeString(value) {
2020
2020
  function createSecretRedactionContext(initialValues = []) {
2021
2021
  const exactSecrets = /* @__PURE__ */ new Set();
2022
2022
  function register(value) {
2023
- if (value.length >= 4) exactSecrets.add(value);
2023
+ if (value.length > 0) exactSecrets.add(value);
2024
2024
  }
2025
2025
  for (const value of initialValues) register(value);
2026
- function redactString2(value) {
2026
+ function containsRegisteredSecret(value, {
2027
+ includeEncoded = false,
2028
+ minimumLength = 1
2029
+ } = {}) {
2030
+ for (const secret of exactSecrets) {
2031
+ if (secret.length < minimumLength) continue;
2032
+ if (value.includes(secret)) return true;
2033
+ if (includeEncoded) {
2034
+ try {
2035
+ const encoded = encodeURIComponent(secret);
2036
+ if (encoded !== secret && value.includes(encoded)) return true;
2037
+ } catch {
2038
+ }
2039
+ }
2040
+ }
2041
+ return false;
2042
+ }
2043
+ function matchesRegisteredSecret(value) {
2044
+ return exactSecrets.has(value);
2045
+ }
2046
+ function redactRegisteredSecrets(value) {
2027
2047
  let output2 = value;
2028
2048
  for (const secret of exactSecrets) {
2029
2049
  output2 = output2.replace(
@@ -2041,6 +2061,10 @@ function createSecretRedactionContext(initialValues = []) {
2041
2061
  } catch {
2042
2062
  }
2043
2063
  }
2064
+ return output2;
2065
+ }
2066
+ function redactString2(value) {
2067
+ const output2 = redactRegisteredSecrets(value);
2044
2068
  return redactSecretLikeString(output2);
2045
2069
  }
2046
2070
  function redact(value) {
@@ -2073,6 +2097,9 @@ function createSecretRedactionContext(initialValues = []) {
2073
2097
  }
2074
2098
  return {
2075
2099
  register,
2100
+ containsRegisteredSecret,
2101
+ matchesRegisteredSecret,
2102
+ redactRegisteredSecrets,
2076
2103
  redactString: redactString2,
2077
2104
  redactKnownSecrets,
2078
2105
  redact
@@ -16692,8 +16719,11 @@ var PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
16692
16719
  ` secrets?: readonly ${cloudReferenceType("bindings.secrets[]")}[];`,
16693
16720
  "};",
16694
16721
  "declare const SECRET_HANDLE_BRAND: unique symbol;",
16722
+ "declare const SECRET_PROMISE_BRAND: unique symbol;",
16695
16723
  "export type SecretHandle = { readonly [SECRET_HANDLE_BRAND]: never; readonly name: string; toString(): string; toJSON(): never };",
16696
- "export type SecretAuth = { readonly kind: 'bearer' | 'header'; readonly secret: SecretHandle; readonly header?: string };",
16724
+ "export type SecretPromise = Promise<string> & { readonly [SECRET_PROMISE_BRAND]: never };",
16725
+ "export type SecretValue = SecretHandle;",
16726
+ "export type SecretAuth = { readonly kind: 'bearer' | 'header'; readonly secret: string | SecretPromise | SecretHandle; readonly header?: string };",
16697
16727
  "export type SecretAuthInput = SecretAuth | readonly SecretAuth[];",
16698
16728
  "export type PlayInputContract<TInput> = { readonly schema: Record<string, unknown>; readonly __inputType?: TInput };",
16699
16729
  "export type PlayReturnObject = Record<string, unknown> & { readonly _metadata?: never };",
@@ -16744,7 +16774,7 @@ var PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
16744
16774
  ` tool<K extends string>(key: ${cloudReferenceType("ctx.tool.key")}, toolId: K, input: ${cloudReferenceType("ctx.tool.input")}, options?: { description?: ${cloudReferenceType("ctx.tool.options.description")} }): Promise<ToolExecutionOutput<K>>;`,
16745
16775
  " step<T>(id: string, run: () => T | Promise<T>, options?: RuntimeStepOptions): Promise<T>;",
16746
16776
  " fetch(key: string, url: string | URL, init?: RequestInit & { headers?: HeadersInit & { 'Idempotency-Key'?: string; 'X-Idempotency-Key'?: string }; auth?: SecretAuthInput }, options?: FetchOptions): Promise<PlayFetchResponse>;",
16747
- " secrets: { get(name: string): SecretHandle; bearer(secret: SecretHandle): SecretAuth; header(header: string, secret: SecretHandle): SecretAuth };",
16777
+ " secrets: { get(name: string): SecretPromise; bearer(secret: string | SecretPromise | SecretHandle): SecretAuth; header(header: string, secret: string | SecretPromise | SecretHandle): SecretAuth };",
16748
16778
  ` runPlay<TOutput = unknown>(key: string, playRef: ${cloudReferenceType("ctx.runPlay.playRef")}, input: ${cloudReferenceType("ctx.runPlay.input")}, options: PlayCallOptions): Promise<TOutput>;`,
16749
16779
  " log(message: string): void;",
16750
16780
  ` sleep(ms: ${cloudReferenceType("ctx.sleep.ms")}): Promise<void>;`,
@@ -16885,15 +16915,118 @@ function listPlayFileExports(sourceCode) {
16885
16915
  return exports2;
16886
16916
  }
16887
16917
 
16888
- // ../shared_libs/plays/docflow.ts
16889
- var MERMAID_NODE_ATTRIBUTES = ["label", "type", "in", "out", "arm"];
16890
- var LEGACY_DOCFLOW_ATTRIBUTES = ["id", ...MERMAID_NODE_ATTRIBUTES];
16891
-
16892
16918
  // ../shared_libs/plays/secret-guardrails.ts
16893
16919
  var SECRET_ENV_PATTERN = /\bprocess(?:\.env|\[['"]env['"]\])(?:\.|\[['"])([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PRIVATE[_-]?KEY|ACCESS[_-]?KEY)[A-Z0-9_]*)(?:['"]\])?/g;
16894
16920
  var PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/;
16895
16921
  var BEARER_LITERAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i;
16896
16922
  var ASSIGNMENT_SECRET_LITERAL_PATTERN = /\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*['"][^'"]{12,}['"]/i;
16923
+ var SECRET_AUTH_LITERAL_FINDING = "literal credential passed to ctx.secrets auth helper";
16924
+ function skipQuoted(source, start) {
16925
+ const quote = source[start];
16926
+ let index = start + 1;
16927
+ while (index < source.length) {
16928
+ if (source[index] === "\\") {
16929
+ index += 2;
16930
+ } else if (source[index] === quote) {
16931
+ return index + 1;
16932
+ } else {
16933
+ index += 1;
16934
+ }
16935
+ }
16936
+ return source.length;
16937
+ }
16938
+ function skipTrivia(source, start) {
16939
+ let index = start;
16940
+ while (index < source.length) {
16941
+ if (/\s/.test(source[index])) {
16942
+ index += 1;
16943
+ } else if (source.startsWith("//", index)) {
16944
+ index = source.indexOf("\n", index + 2);
16945
+ if (index === -1) return source.length;
16946
+ } else if (source.startsWith("/*", index)) {
16947
+ index = source.indexOf("*/", index + 2);
16948
+ if (index === -1) return source.length;
16949
+ index += 2;
16950
+ } else {
16951
+ return index;
16952
+ }
16953
+ }
16954
+ return index;
16955
+ }
16956
+ function consumeIdentifier(source, start, expected) {
16957
+ return source.startsWith(expected, start) && !/[A-Za-z0-9_$]/.test(source[start - 1] ?? "") && !/[A-Za-z0-9_$]/.test(source[start + expected.length] ?? "") ? start + expected.length : void 0;
16958
+ }
16959
+ function consumeMember(source, start, expected) {
16960
+ let index = skipTrivia(source, start);
16961
+ if (source[index] === ".") {
16962
+ return consumeIdentifier(source, skipTrivia(source, index + 1), expected);
16963
+ }
16964
+ if (source[index] !== "[") return void 0;
16965
+ index = skipTrivia(source, index + 1);
16966
+ if (source[index] !== "'" && source[index] !== '"') return void 0;
16967
+ const end = skipQuoted(source, index);
16968
+ if (source.slice(index + 1, end - 1) !== expected) return void 0;
16969
+ index = skipTrivia(source, end);
16970
+ return source[index] === "]" ? index + 1 : void 0;
16971
+ }
16972
+ function skipExpression(source, start) {
16973
+ let index = start;
16974
+ let depth = 0;
16975
+ while (index < source.length) {
16976
+ const char = source[index];
16977
+ if (char === "'" || char === '"' || char === "`") {
16978
+ index = skipQuoted(source, index);
16979
+ continue;
16980
+ }
16981
+ if (source.startsWith("//", index) || source.startsWith("/*", index)) {
16982
+ index = skipTrivia(source, index);
16983
+ continue;
16984
+ }
16985
+ if (char === "(" || char === "[" || char === "{") {
16986
+ depth += 1;
16987
+ } else if (char === ")" || char === "]" || char === "}") {
16988
+ if (depth === 0) return index;
16989
+ depth -= 1;
16990
+ } else if (char === "," && depth === 0) {
16991
+ return index;
16992
+ }
16993
+ index += 1;
16994
+ }
16995
+ return index;
16996
+ }
16997
+ function isDirectStringLiteral(source, start) {
16998
+ const quote = source[start];
16999
+ if (quote !== "'" && quote !== '"' && quote !== "`") return false;
17000
+ const end = skipQuoted(source, start);
17001
+ return quote !== "`" || !source.slice(start, end).includes("${");
17002
+ }
17003
+ function hasLiteralSecretAuthCredential(source) {
17004
+ for (let index = 0; index < source.length; index += 1) {
17005
+ const char = source[index];
17006
+ if (char === "'" || char === '"' || char === "`") {
17007
+ index = skipQuoted(source, index) - 1;
17008
+ continue;
17009
+ }
17010
+ if (source.startsWith("//", index) || source.startsWith("/*", index)) {
17011
+ index = skipTrivia(source, index) - 1;
17012
+ continue;
17013
+ }
17014
+ const afterCtx = consumeIdentifier(source, index, "ctx");
17015
+ if (!afterCtx) continue;
17016
+ const afterSecrets = consumeMember(source, afterCtx, "secrets");
17017
+ if (!afterSecrets) continue;
17018
+ const afterHeader = consumeMember(source, afterSecrets, "header");
17019
+ const helper = afterHeader ? "header" : "bearer";
17020
+ const afterHelper = afterHeader ?? consumeMember(source, afterSecrets, "bearer");
17021
+ if (!afterHelper) continue;
17022
+ const afterOpen = skipTrivia(source, afterHelper);
17023
+ if (source[afterOpen] !== "(") continue;
17024
+ const firstArgument = skipTrivia(source, afterOpen + 1);
17025
+ const credential = helper === "header" ? skipTrivia(source, skipExpression(source, firstArgument) + 1) : firstArgument;
17026
+ if (isDirectStringLiteral(source, credential)) return true;
17027
+ }
17028
+ return false;
17029
+ }
16897
17030
  function collectInlineSecretFindings(sourceCode) {
16898
17031
  const findings = [];
16899
17032
  for (const match of sourceCode.matchAll(SECRET_ENV_PATTERN)) {
@@ -16905,6 +17038,9 @@ function collectInlineSecretFindings(sourceCode) {
16905
17038
  if (ASSIGNMENT_SECRET_LITERAL_PATTERN.test(sourceCode)) {
16906
17039
  findings.push("secret-looking assignment literal");
16907
17040
  }
17041
+ if (hasLiteralSecretAuthCredential(sourceCode)) {
17042
+ findings.push(SECRET_AUTH_LITERAL_FINDING);
17043
+ }
16908
17044
  return [...new Set(findings)];
16909
17045
  }
16910
17046
 
@@ -25254,6 +25390,14 @@ Concepts:
25254
25390
  bundle local .play.ts files for development workflows.
25255
25391
  Running a local file does not make it live; use publish or set-live for that.
25256
25392
 
25393
+ Authoring:
25394
+ Use normal TypeScript interpolation for human-facing strings:
25395
+ const readyMessage = \`Put \${input.title} through to send-ready.\`;
25396
+ An authored diagram is optional. Docflow parses only an explicit
25397
+ /** @mermaid ... */ block and its // @mermaid-node annotations; ordinary
25398
+ prose comments are never Docflow syntax. When Docflow is enabled, plays check
25399
+ gives missing diagrams a non-blocking visualization warning.
25400
+
25257
25401
  Common commands:
25258
25402
  deepline plays search email --json
25259
25403
  deepline plays describe prebuilt/person-linkedin-to-email --json