deepline 0.1.67 → 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.
- package/dist/cli/index.js +1027 -577
- package/dist/cli/index.mjs +1027 -577
- package/dist/index.d.mts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +18 -2
- package/dist/index.mjs +18 -2
- package/dist/repo/apps/play-runner-workers/src/entry.ts +122 -29
- package/dist/repo/sdk/src/client.ts +37 -0
- package/dist/repo/sdk/src/play.ts +33 -1
- package/dist/repo/sdk/src/plays/bundle-play-file.ts +4 -1
- package/dist/repo/sdk/src/release.ts +2 -2
- package/dist/repo/shared_libs/play-runtime/secret-capability.ts +103 -0
- package/dist/repo/shared_libs/play-runtime/secret-redaction.ts +90 -0
- package/dist/repo/shared_libs/plays/bundling/index.ts +10 -0
- package/dist/repo/shared_libs/plays/secret-guardrails.ts +57 -0
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const SECRET_REDACTION_PLACEHOLDER = '[REDACTED_SECRET]';
|
|
2
|
+
|
|
3
|
+
const BEARER_TOKEN_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/g;
|
|
4
|
+
const JWT_RE =
|
|
5
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
6
|
+
const PRIVATE_KEY_RE =
|
|
7
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g;
|
|
8
|
+
const COMMON_SECRET_RE =
|
|
9
|
+
/\b(?:sk|pk|rk|pat|ghp|github_pat|xox[baprs]|key|token|secret|api[_-]?key)[A-Za-z0-9_./+=:-]{12,}\b/gi;
|
|
10
|
+
const HIGH_ENTROPY_ASSIGNMENT_RE =
|
|
11
|
+
/\b(?:api[_-]?key|token|secret|password|authorization|access[_-]?token|refresh[_-]?token)\b\s*[:=]\s*["']?[^"',\s]{16,}["']?/gi;
|
|
12
|
+
|
|
13
|
+
function escapeRegExp(value: string): string {
|
|
14
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function redactSecretLikeString(value: string): string {
|
|
22
|
+
return value
|
|
23
|
+
.replace(PRIVATE_KEY_RE, SECRET_REDACTION_PLACEHOLDER)
|
|
24
|
+
.replace(BEARER_TOKEN_RE, `Bearer ${SECRET_REDACTION_PLACEHOLDER}`)
|
|
25
|
+
.replace(JWT_RE, SECRET_REDACTION_PLACEHOLDER)
|
|
26
|
+
.replace(COMMON_SECRET_RE, SECRET_REDACTION_PLACEHOLDER)
|
|
27
|
+
.replace(HIGH_ENTROPY_ASSIGNMENT_RE, (match) => {
|
|
28
|
+
const separator = match.includes('=') ? '=' : ':';
|
|
29
|
+
const [key] = match.split(separator, 1);
|
|
30
|
+
return `${key.trim()}${separator}${SECRET_REDACTION_PLACEHOLDER}`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SecretRedactionContext = {
|
|
35
|
+
register(value: string): void;
|
|
36
|
+
redactString(value: string): string;
|
|
37
|
+
redact<T>(value: T): T;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function createSecretRedactionContext(
|
|
41
|
+
initialValues: readonly string[] = [],
|
|
42
|
+
): SecretRedactionContext {
|
|
43
|
+
const exactSecrets = new Set<string>();
|
|
44
|
+
|
|
45
|
+
function register(value: string): void {
|
|
46
|
+
if (value.length >= 4) exactSecrets.add(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const value of initialValues) register(value);
|
|
50
|
+
|
|
51
|
+
function redactString(value: string): string {
|
|
52
|
+
let output = value;
|
|
53
|
+
for (const secret of exactSecrets) {
|
|
54
|
+
output = output.replace(
|
|
55
|
+
new RegExp(escapeRegExp(secret), 'g'),
|
|
56
|
+
SECRET_REDACTION_PLACEHOLDER,
|
|
57
|
+
);
|
|
58
|
+
try {
|
|
59
|
+
const encoded = encodeURIComponent(secret);
|
|
60
|
+
if (encoded !== secret) {
|
|
61
|
+
output = output.replace(
|
|
62
|
+
new RegExp(escapeRegExp(encoded), 'g'),
|
|
63
|
+
SECRET_REDACTION_PLACEHOLDER,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// encodeURIComponent only fails on malformed surrogate pairs. Exact
|
|
68
|
+
// redaction above still applies in that rare case.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return redactSecretLikeString(output);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function redact<T>(value: T): T {
|
|
75
|
+
if (typeof value === 'string') return redactString(value) as T;
|
|
76
|
+
if (Array.isArray(value)) return value.map((entry) => redact(entry)) as T;
|
|
77
|
+
if (isRecord(value)) {
|
|
78
|
+
return Object.fromEntries(
|
|
79
|
+
Object.entries(value).map(([key, entry]) => [key, redact(entry)]),
|
|
80
|
+
) as T;
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
register,
|
|
87
|
+
redactString,
|
|
88
|
+
redact,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
PlayRuntimeFeature,
|
|
26
26
|
} from '../artifact-types';
|
|
27
27
|
import { buildPlayContractCompatibility } from '../contracts';
|
|
28
|
+
import { validatePlaySourceFilesHaveNoInlineSecrets } from '../secret-guardrails';
|
|
28
29
|
|
|
29
30
|
const PLAY_BUNDLE_CACHE_VERSION = 24;
|
|
30
31
|
const MAX_PLAY_BUNDLE_BYTES = 30 * 1024 * 1024;
|
|
@@ -1450,6 +1451,15 @@ export async function bundlePlayFile(
|
|
|
1450
1451
|
`${analysis.graphHash}\nworkers-harness:${harnessFingerprint}`,
|
|
1451
1452
|
);
|
|
1452
1453
|
}
|
|
1454
|
+
try {
|
|
1455
|
+
validatePlaySourceFilesHaveNoInlineSecrets(analysis.sourceFiles);
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
return {
|
|
1458
|
+
success: false,
|
|
1459
|
+
filePath: absolutePath,
|
|
1460
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1453
1463
|
const typecheckErrors = [
|
|
1454
1464
|
...((await adapter.typecheckPlaySource?.({
|
|
1455
1465
|
sourceCode: analysis.sourceCode,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const SECRET_ENV_PATTERN =
|
|
2
|
+
/\bprocess(?:\.env|\[['"]env['"]\])(?:\.|\[['"])([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PRIVATE[_-]?KEY|ACCESS[_-]?KEY)[A-Z0-9_]*)(?:['"]\])?/g;
|
|
3
|
+
const PRIVATE_KEY_PATTERN =
|
|
4
|
+
/-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/;
|
|
5
|
+
const BEARER_LITERAL_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/i;
|
|
6
|
+
const ASSIGNMENT_SECRET_LITERAL_PATTERN =
|
|
7
|
+
/\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*['"][^'"]{12,}['"]/i;
|
|
8
|
+
const HIGH_ENTROPY_LITERAL_PATTERN =
|
|
9
|
+
/['"]([A-Za-z0-9+/=_-]{32,})['"]/g;
|
|
10
|
+
|
|
11
|
+
function shannonEntropy(value: string): number {
|
|
12
|
+
const counts = new Map<string, number>();
|
|
13
|
+
for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
14
|
+
return [...counts.values()].reduce((entropy, count) => {
|
|
15
|
+
const p = count / value.length;
|
|
16
|
+
return entropy - p * Math.log2(p);
|
|
17
|
+
}, 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validatePlaySourceHasNoInlineSecrets(input: {
|
|
21
|
+
sourceCode: string;
|
|
22
|
+
filePath: string;
|
|
23
|
+
}): void {
|
|
24
|
+
const findings: string[] = [];
|
|
25
|
+
for (const match of input.sourceCode.matchAll(SECRET_ENV_PATTERN)) {
|
|
26
|
+
findings.push(`process.env.${match[1]}`);
|
|
27
|
+
}
|
|
28
|
+
if (PRIVATE_KEY_PATTERN.test(input.sourceCode)) findings.push('private key block');
|
|
29
|
+
if (BEARER_LITERAL_PATTERN.test(input.sourceCode)) findings.push('bearer token literal');
|
|
30
|
+
if (ASSIGNMENT_SECRET_LITERAL_PATTERN.test(input.sourceCode)) {
|
|
31
|
+
findings.push('secret-looking assignment literal');
|
|
32
|
+
}
|
|
33
|
+
for (const match of input.sourceCode.matchAll(HIGH_ENTROPY_LITERAL_PATTERN)) {
|
|
34
|
+
const literal = match[1] ?? '';
|
|
35
|
+
if (literal.length >= 40 && shannonEntropy(literal) >= 4.2) {
|
|
36
|
+
findings.push('high-entropy string literal');
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!findings.length) return;
|
|
41
|
+
throw new Error(
|
|
42
|
+
[
|
|
43
|
+
`Play source ${input.filePath} appears to contain inline secret material: ${[
|
|
44
|
+
...new Set(findings),
|
|
45
|
+
].join(', ')}.`,
|
|
46
|
+
'Author secrets in the dashboard and use ctx.secrets.get("NAME") with an approved helper such as ctx.secrets.bearer(handle).',
|
|
47
|
+
].join(' '),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function validatePlaySourceFilesHaveNoInlineSecrets(
|
|
52
|
+
sourceFiles: Record<string, string>,
|
|
53
|
+
): void {
|
|
54
|
+
for (const [filePath, sourceCode] of Object.entries(sourceFiles)) {
|
|
55
|
+
validatePlaySourceHasNoInlineSecrets({ filePath, sourceCode });
|
|
56
|
+
}
|
|
57
|
+
}
|